From 0da7957230ee0e010b7663c1e8e871a08da54289 Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 18 Sep 2024 10:33:10 +0800 Subject: [PATCH 01/81] =?UTF-8?q?im=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 6 +- android/build.gradle | 4 +- android/gradle.properties | 4 +- lib/im/SocketClient.dart | 29 +- lib/im/chat_details_page.dart | 727 ++++++++++++++++------------- lib/im/database/hx_database.dart | 125 ++++- lib/im/database/hx_database.g.dart | 290 ++++++------ lib/im/database/message.dart | 24 +- lib/im/database/message_dao.dart | 34 +- lib/im/database/migration.dart | 41 ++ lib/im/im_view/im_page.dart | 33 +- lib/im/out/auth.pb.dart | 16 +- lib/im/out/auth.pbjson.dart | 8 +- lib/im/out/message.pb.dart | 16 +- lib/im/out/message.pbjson.dart | 6 +- lib/main.dart | 5 +- lib/mine/edit_signature.dart | 1 - pubspec.lock | 42 +- pubspec.yaml | 3 +- 19 files changed, 804 insertions(+), 610 deletions(-) create mode 100644 lib/im/database/migration.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 67b0c9de..9c33dd25 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply plugin: 'com.mob.sdk' -apply plugin: 'com.huawei.agconnect' +//apply plugin: 'com.huawei.agconnect' MobSDK { appKey "m33ee7650da86a" @@ -203,7 +203,7 @@ dependencies { implementation 'com.tencent.tpns:xiaomi:1.2.7.1-release' - implementation 'com.tencent.tpns:huawei:1.2.6.0-release' +// implementation 'com.tencent.tpns:huawei:1.2.6.0-release' // HMS Core Push 模块依赖包 - implementation 'com.huawei.hms:push:5.3.0.304' +// implementation 'com.huawei.hms:push:5.3.0.304' } diff --git a/android/build.gradle b/android/build.gradle index 4bdad802..ffc4623c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.10' repositories { maven { url "https://www.jitpack.io" } maven {url 'https://developer.huawei.com/repo/'} @@ -24,7 +24,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.huawei.agconnect:agcp:1.4.1.300' +// classpath 'com.huawei.agconnect:agcp:1.4.1.300' classpath 'com.mob.sdk:MobSDK:+' classpath fileTree(include:['*.jar'], dir:'libs') classpath 'com.umeng.umsdk:common:9.4.7' diff --git a/android/gradle.properties b/android/gradle.properties index ec92ecce..51136ae4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ -org.gradle.jvmargs=-Xmx1536M -#org.gradle.jvmargs=-Xmx4096m +#org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096m android.useAndroidX=true android.enableJetifier=true MobSDK.mobEnv=x diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 5f148dfb..160108c4 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -27,11 +27,10 @@ class SocketClient { print(data); print("socket-listen"); Proto proto = Proto.fromBytes(data); - print("socket-listen: $proto"); MsgData data1 = MsgData.fromBuffer(proto.body); print('收到来自:${data1.from},消息内容: ${utf8.decode(data1.data)} '); - hxDatabase.messageDao.insertMessage(createMessage(mobile, utf8.decode(data1.data), userId: data1.from)); + hxDatabase.insert(createMessage(userId, utf8.decode(data1.data), msgType: data1.type.value, userId: data1.from)); callbacks.forEach((callback) { callback.call(data1); @@ -67,7 +66,7 @@ class SocketClient { return; } final authReq = AuthReq() - ..uid = mobile + ..uid = userId ..token = token; final authReqBytes = authReq.writeToBuffer(); final proto = Proto(1, 1, authReqBytes); // 假设 operation 和 seqId 为 1 @@ -75,18 +74,26 @@ class SocketClient { _socket.add(protoBytes); } - sendMessage(int toId, String content) { + Future sendMessage(String toId, String content) async { + Map message = createMessage(toId, content, userId: userId); + int id = await hxDatabase.insert(message).catchError((error) { + debugPrint("insertMessage: $error"); + }); if (!checkSocket()) { - return; + hxDatabase.update({"id": id, "state": 3}).catchError((error) { + debugPrint("insertMessage: $error"); + }); + message["id"] = id; + message["state"] = 3; + return Message.fromJson(message); } + message["id"] = id; Uint8List data = utf8.encode(content); - MsgData msgData = MsgData(to: toId, from: mobile, type: MsgType.SINGLE_TEXT,data: data); + MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.SINGLE_TEXT, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); _socket.add(proto2.toBytes()); - hxDatabase.messageDao.insertMessage(createMessage(toId, content, userId: mobile)).catchError((error) { - debugPrint("insertMessage: $error"); - }); - debugPrint("insertMessage: end"); + debugPrint("sendMessage: ${message["id"]}"); + return Message.fromJson(message); } checkSocket() { @@ -97,7 +104,7 @@ class SocketClient { return true; } - get mobile => 123456; + get userId => shared.getString("userId"); } \ No newline at end of file diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 695616ae..42e5c231 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -7,11 +7,13 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; import 'package:image_pickers/image_pickers.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; @@ -58,6 +60,19 @@ class _ChatDetailsPage extends State // SmartDialog.showToast("聊天 $txt", alignment: Alignment.center); } + List messages = []; + + loadMessageList() async { + selfUserId = (await SharedPreferences.getInstance()).getString("userId"); + + toUserId = widget.arguments["toId"]; + messages = await hxDatabase.queryUList(toUserId); + if (mounted) setState(() {}); + } + + String selfUserId = ""; + String toUserId = ""; + @override void initState() { super.initState(); @@ -65,20 +80,21 @@ class _ChatDetailsPage extends State WidgetsBinding.instance.addObserver(this); commentFocus.addListener(_focusNodeListener); + loadMessageList(); + Future.delayed(Duration.zero, () { jumpToBottom(); }); } - void jumpToBottom(){ - scrollController.position - .jumpTo(scrollController.position.maxScrollExtent); + void jumpToBottom() { + scrollController.position.jumpTo(scrollController.position.maxScrollExtent); } void didChangeMetrics() { WidgetsBinding.instance.addPostFrameCallback((_) { - isKeyBoardShow = MediaQuery.of(context).viewInsets.bottom > 0; if (!mounted) return; + isKeyBoardShow = MediaQuery.of(context).viewInsets.bottom > 0; if (MediaQuery.of(context).viewInsets.bottom == 0) { if (isKeyBoardShow) { FocusScope.of(context).requestFocus(FocusNode()); @@ -212,7 +228,7 @@ class _ChatDetailsPage extends State double h = MediaQuery.of(context).viewInsets.bottom; if (h > 0) { jumpToBottom(); - if(keyboard < h){ + if (keyboard < h) { keyboard = h; // setState(() {}); } @@ -241,7 +257,8 @@ class _ChatDetailsPage extends State color: Colors.black, size: 30, ), - )), + ), + ), ), body: Container( child: Column( @@ -263,7 +280,8 @@ class _ChatDetailsPage extends State FocusScope.of(context).requestFocus(FocusNode()); }); }, - child: chatDetailsList()), + child: chatDetailsList(), + ), ], ), ), @@ -281,26 +299,32 @@ class _ChatDetailsPage extends State return Container( margin: EdgeInsets.only(bottom: 48.h), child: ListView.builder( - itemCount: 10, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, position) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState((){ - copyIndex = 0; - }); - }, - child: chatDetailsItem(), - ); - }), + itemCount: messages.length, + shrinkWrap: true, + reverse: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, position) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + copyIndex = 0; + }); + }, + child: chatDetailsItem(messages[position]), + ); + }, + ), ); } - Widget chatDetailsItem() { + Widget chatDetailsItem(Message message) { + bool isSelf = message.fromId == selfUserId; + bool isText = message.msgType == 0; return Container( - padding: EdgeInsets.only(top: 32.h,), + padding: EdgeInsets.only( + top: 32.h, + ), child: Column( children: [ Text( @@ -312,7 +336,8 @@ class _ChatDetailsPage extends State fontWeight: MyFontWeight.regular, ), ), - Padding( + if (messages.indexOf(message) == (messages.length - 1)) + Padding( padding: EdgeInsets.only(top: 10.h, bottom: 24.h), child: Text( "在对方未回复或关注你之前,你只能发送一条信息", @@ -324,175 +349,46 @@ class _ChatDetailsPage extends State ), ), ), - SizedBox( - height: 16.h, - ), - if(copyIndex == 1) - Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - padding: EdgeInsets.only(bottom:13.h), - child: Container( - width: 180.w, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric(horizontal: 32.w,vertical: 7.5.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: (){ - setState((){ - copyIndex = 0; - this.copy(tex); - }); - }, - child:Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height:16, - width: 16, - ), - SizedBox(height: 2.h,), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ) , - ), - GestureDetector( - onTap: (){}, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height:16, - width: 16, - ), - SizedBox(height: 2.h,), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ) - ], - ), - ), - ), - Image.asset( - "assets/image/icon_copy_j.webp", - height: 17, - width: 17, - ), - ], - ), - Padding(padding:EdgeInsets.only(left: 17.w,right: 39.w), - child: Row( - children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44.h, - width: 44.h, - ), - SizedBox( - width: 12.w, - ), - Expanded( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ) - ], - ), - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), - child:GestureDetector( - onLongPress:(){ - setState((){ - copyIndex =1; - }); - }, - child: - Text( - tex = "上次你在我这里买的水果钱是不是忘记付了?一共18块钱做点生意也是真的不容易啊。", - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Color(0XFF0D0D0D), - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, - ), - ), - ) - )), - ], - ),), - SizedBox(height: 40.h,), - if(copyIndex == 1) - Stack( + if (messages.indexOf(message) == (messages.length - 1)) + SizedBox( + height: 16.h, + ), + if (copyIndex == 1) + Stack( alignment: Alignment.bottomCenter, children: [ Container( - padding: EdgeInsets.only(bottom:13.h), + padding: EdgeInsets.only(bottom: 13.h), child: Container( width: 180.w, decoration: BoxDecoration( color: Color(0xFF2A2A2A), borderRadius: BorderRadius.circular(6), ), - padding: EdgeInsets.symmetric(horizontal: 32.w,vertical: 7.5.h), + padding: EdgeInsets.symmetric( + horizontal: 32.w, vertical: 7.5.h, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( - onTap: (){ - setState((){ + onTap: () { + setState(() { copyIndex = 0; this.copy(tex); }); }, - child:Column( + child: Column( children: [ Image.asset( "assets/image/icon_chat_copy.webp", - height:16, + height: 16, width: 16, ), - SizedBox(height: 2.h,), + SizedBox( + height: 2.h, + ), Text( "复制", textAlign: TextAlign.center, @@ -503,18 +399,20 @@ class _ChatDetailsPage extends State ), ), ], - ) , + ), ), GestureDetector( - onTap: (){}, + onTap: () {}, child: Column( children: [ Image.asset( "assets/image/icon_chat_delete.webp", - height:16, + height: 16, width: 16, ), - SizedBox(height: 2.h,), + SizedBox( + height: 2.h, + ), Text( S.of(context).shanchu, textAlign: TextAlign.center, @@ -538,127 +436,293 @@ class _ChatDetailsPage extends State ), ], ), - Padding(padding:EdgeInsets.only(left:36.w,right: 16.w), + + /// not self + if (!isSelf && isText) + Padding( + padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Color(0xFFFF441A), - ), - width: 20, - height: 20, - alignment: Alignment.center, - child: Text( - "!", - textAlign: TextAlign.left, - style: TextStyle( - color: Colors.white, - fontSize: 17.sp, - fontWeight: MyFontWeight.regular, + children: [ + // MImage( + // "", + // isCircle: true, + // width: 44, + // height: 44, + // fit: BoxFit.cover, + // errorSrc: "assets/image/default_user.webp", + // fadeSrc: "assets/image/default_user.webp", + // ), + Image.asset( + "assets/image/fuka_zj.webp", + height: 44.h, + width: 44.h, ), - ), - ), - SizedBox( - width: 12.w, + SizedBox( + width: 12.w, + ), + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ) + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, horizontal: 12.w), + child: GestureDetector( + onLongPress: () { + setState(() { + copyIndex = 1; + }); + }, + child: Text( + tex = message.content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Color(0XFF0D0D0D), + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), + ), + ))), + ], ), - Expanded( + ), + if (!isSelf && isText) + SizedBox( + height: 40.h, + ), + if (copyIndex == 1) + Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.only(bottom: 13.h), child: Container( + width: 180.w, decoration: BoxDecoration( + color: Color(0xFF2A2A2A), borderRadius: BorderRadius.circular(6), - color: Color(0xFF32A060), ), - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), - child: GestureDetector( - onLongPress:(){ - setState((){ - copyIndex = 1; - }); - }, - child: - Text( - tex = "凭本事买的为啥要给你钱啊伙计", + padding: + EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() { + copyIndex = 0; + this.copy(tex); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + "复制", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () {}, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ) + ], + ), + ), + ), + Image.asset( + "assets/image/icon_copy_j.webp", + height: 17, + width: 17, + ), + ], + ), + + /// self + if (isSelf && isText) + Padding( + padding: EdgeInsets.only(left: 36.w, right: 16.w), + child: Row( + children: [ + if (message.state == 3) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + width: 20, + height: 20, + alignment: Alignment.center, + child: Text( + "!", textAlign: TextAlign.left, style: TextStyle( - height: 1.2.h, color: Colors.white, fontSize: 17.sp, - fontWeight: MyFontWeight.medium, + fontWeight: MyFontWeight.regular, ), ), ), - )), - SizedBox( - width: 12.w, - ), - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44, - width: 44, - ), - ], - )), - Padding(padding:EdgeInsets.only(left: 17.w,right: 39.w,top: 20.h), - child: Row( - children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44.h, - width: 44.h, - ), - SizedBox( - width: 12.w, - ), - Expanded( - flex: 3, + if (message.state == 3) + SizedBox( + width: 12.w, + ), + Expanded( child: Container( + alignment: Alignment.centerRight, + child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ) - ], + color: Color(0xFF32A060), ), - child:GestureDetector( - onLongPress:(){ - setState((){ + padding: + EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), + child: GestureDetector( + onLongPress: () { + setState(() { + copyIndex = 1; }); }, - child: Image.asset( - "assets/image/icon_guide_4.webp", - width:180.h, - height: 200.h, - fit: BoxFit.fill, + child: Text( + tex = message.content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Colors.white, + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), ), - ) - )), - Expanded(flex:1,child: Container(),) - ], - ),), - Padding( + ), + ), + ), + ), + SizedBox( + width: 12.w, + ), + // MImage( + // "", + // isCircle: true, + // width: 44, + // height: 44, + // fit: BoxFit.cover, + // errorSrc: "assets/image/default_user.webp", + // fadeSrc: "assets/image/default_user.webp", + // ), + Image.asset( + "assets/image/fuka_zj.webp", + height: 44, + width: 44, + ), + ], + ), + ), + + /// not self image + if (!isSelf && !isText) + Padding( + padding: EdgeInsets.only(left: 17.w, right: 39.w, top: 20.h), + child: Row( + children: [ + // MImage( + // "", + // isCircle: true, + // width: 44, + // height: 44, + // fit: BoxFit.cover, + // errorSrc: "assets/image/default_user.webp", + // fadeSrc: "assets/image/default_user.webp", + // ), + Image.asset( + "assets/image/fuka_zj.webp", + height: 44.h, + width: 44.h, + ), + SizedBox( + width: 12.w, + ), + Expanded( + flex: 3, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ) + ], + ), + child: GestureDetector( + onLongPress: () { + setState(() {}); + }, + child: Image.asset( + "assets/image/icon_guide_4.webp", + width: 180.h, + height: 200.h, + fit: BoxFit.fill, + ), + ))), + Expanded( + flex: 1, + child: Container(), + ) + ], + ), + ), + + /// no reply long time + if (messages.indexOf(message) == 0) + Padding( padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), child: Text( "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", @@ -679,7 +743,7 @@ class _ChatDetailsPage extends State Widget input() { return Container( color: Color(0xFFFDFCFC), - padding: EdgeInsets.only(top:14.h, bottom: 15.h), + padding: EdgeInsets.only(top: 14.h, bottom: 15.h), child: Column( children: [ ///输入框 @@ -696,7 +760,7 @@ class _ChatDetailsPage extends State borderRadius: BorderRadius.circular(6), ), child: Container( - margin: EdgeInsets.only(left:17.w), + margin: EdgeInsets.only(left: 17.w), alignment: Alignment.topLeft, child: TextField( textInputAction: TextInputAction.send, @@ -709,6 +773,13 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } + socketClient.sendMessage(toUserId, commentText).then((value) { + Message message = value; + messages.insert(0, message); + chatController.clear(); + if (mounted) + setState(() {}); + }); // widget.queryMemberComment(commentText); }, maxLines: 8, @@ -818,7 +889,7 @@ class _ChatDetailsPage extends State width: double.infinity, height: 1.h, color: Color(0xFFF7F7F7), - margin:EdgeInsets.only(bottom:10.h), + margin: EdgeInsets.only(bottom: 10.h), ), Row( children: [ @@ -842,7 +913,7 @@ class _ChatDetailsPage extends State ) ], ), - margin:EdgeInsets.only(right: 15.w,left: 14.w), + margin: EdgeInsets.only(right: 15.w, left: 14.w), padding: EdgeInsets.all(12), child: Image.asset( "assets/image/icon_chat_camera.webp", @@ -850,53 +921,60 @@ class _ChatDetailsPage extends State width: 24, ), ), - Padding(padding:EdgeInsets.only(top: 8.h), - child: Text( - "拍照", - style: TextStyle( - fontSize:12.sp, color: Color(0xFFA29E9E), - fontWeight: MyFontWeight.regular,), - ),), + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + "拍照", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFA29E9E), + fontWeight: MyFontWeight.regular, + ), + ), + ), ], ), ), GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - getImageOrVideo(GalleryMode.image); - }, - child: Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFD5D5D5).withAlpha(25), - offset: Offset(4, 2), - blurRadius: 4, - spreadRadius: 0, - ) - ], + behavior: HitTestBehavior.opaque, + onTap: () { + getImageOrVideo(GalleryMode.image); + }, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFD5D5D5).withAlpha(25), + offset: Offset(4, 2), + blurRadius: 4, + spreadRadius: 0, + ) + ], + ), + padding: EdgeInsets.all(12), + child: Image.asset( + "assets/image/icon_chat_photo.webp", + height: 24, + width: 24, + ), ), - padding: EdgeInsets.all(12), - child: Image.asset( - "assets/image/icon_chat_photo.webp", - height: 24, - width: 24, + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + "相册", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFA29E9E), + fontWeight: MyFontWeight.regular, + ), + ), ), - ), - Padding(padding:EdgeInsets.only(top: 8.h), - child: Text( - "相册", - style: TextStyle( - fontSize:12.sp, color: Color(0xFFA29E9E), - fontWeight: MyFontWeight.regular,), - ),), - ], - ) - ), + ], + )), ], ) ], @@ -908,14 +986,17 @@ class _ChatDetailsPage extends State ); } - showCustomDialog(BuildContext context,int position ){ + showCustomDialog(BuildContext context, int position) { showDialog( context: context, builder: (BuildContext context) { return new AlertDialog( backgroundColor: Color(0xFF2A2A2A), elevation: 0, - contentPadding:EdgeInsets.only(top: 8.h,bottom: 5.h,), + contentPadding: EdgeInsets.only( + top: 8.h, + bottom: 5.h, + ), content: Container( height: 40.h, // width:20.w, @@ -928,7 +1009,7 @@ class _ChatDetailsPage extends State children: [ Image.asset( "assets/image/icon_chat_copy.webp", - height:16, + height: 16, width: 16, ), Text( @@ -946,7 +1027,7 @@ class _ChatDetailsPage extends State children: [ Image.asset( "assets/image/icon_chat_delete.webp", - height:16, + height: 16, width: 16, ), Text( diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 9ac61cbb..460e63de 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -1,18 +1,123 @@ - -import 'dart:async'; - -import 'package:floor/floor.dart'; import 'package:flutter/cupertino.dart'; -import 'package:huixiang/im/database/message_dao.dart'; import 'package:huixiang/im/database/message.dart'; -import 'package:sqflite/sqflite.dart' as sqflite; +import 'package:huixiang/im/database/migration.dart'; +import 'package:sqflite/sqflite.dart'; + + +class HxDatabase { + + Database db; + + void open() async { + + // _migrations.add(Migration(1, 2, (Database database) async { + // database.execute('ALTER TABLE `Message` ADD COLUMN `replyId` VARCHAR(20) DEFAULT NULL AFTER `toId`'); + // })); + + await openDatabase( + 'hx.db', + version: 2, + onCreate: (Database db, int version) async { + db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + }, + onConfigure: (database) async { + await database.execute('PRAGMA foreign_keys = ON'); + }, + onUpgrade: (database, startVersion, endVersion) async { + await runMigrations(database, startVersion, endVersion, _migrations); + }, + onOpen: (Database db) { + this.db = db; + } + ); + } + + void close() { + db.close(); + } + + Future> queryList(userId) { + if (db == null) { + return Future.value([]); + } + String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? GROUP BY toId,fromId ORDER BY time DESC'; + return db.rawQuery(sql, [userId, userId]).then((value) { + return value.map((e) { + debugPrint("Message: ${e}"); + return Message.fromJson(e); + }).toList(); + }, onError: (error) { + debugPrint("Messageerror: $error"); + }); + } + + Future> queryUList(userId) { + if (db == null) { + return Future.value([]); + } + String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC'; + return db.rawQuery(sql, [userId, userId]).then((value) { + return value.map((e) => Message.fromJson(e)).toList(); + }, onError: (error) { + debugPrint("Messageerror: $error"); + }); + } + + Future> queryListAll() { + if (db == null) { + return Future.value(); + } + String sql = 'SELECT * FROM Message ORDER BY time DESC'; + return db.rawQuery(sql); + } + + Future deleteAll() async { + return db.delete("Message"); + } + + update(Map message) { + + } + + Future insert(Map message) async { + if (db == null) { + return Future.value(0); + } + debugPrint("Messageinsert: ${message}"); + return db.insert("Message", message); + } + + final List _migrations = []; + + addMigrations(List migrations) { + _migrations.addAll(migrations); + return this; + } -part 'hx_database.g.dart'; + Future runMigrations( + final Database migrationDatabase, + final int startVersion, + final int endVersion, + final List migrations, + ) async { + final relevantMigrations = migrations + .where((migration) => migration.startVersion >= startVersion) + .toList() + ..sort( + (first, second) => first.startVersion.compareTo(second.startVersion)); -@Database(version: 1, entities: [Message]) -abstract class HxDatabase extends FloorDatabase { + if (relevantMigrations.isEmpty || + relevantMigrations.last.endVersion != endVersion) { + throw StateError( + 'There is no migration supplied to update the database to the current version.' + ' Aborting the migration.', + ); + } - MessageDao get messageDao; + for (final migration in relevantMigrations) { + await migration.migrate(migrationDatabase); + } + } } diff --git a/lib/im/database/hx_database.g.dart b/lib/im/database/hx_database.g.dart index b0ba6d66..ce84ec59 100644 --- a/lib/im/database/hx_database.g.dart +++ b/lib/im/database/hx_database.g.dart @@ -1,144 +1,146 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'hx_database.dart'; - -// ************************************************************************** -// FloorGenerator -// ************************************************************************** - -// ignore: avoid_classes_with_only_static_members -class $FloorHxDatabase { - /// Creates a database builder for a persistent database. - /// Once a database is built, you should keep a reference to it and re-use it. - static _$HxDatabaseBuilder databaseBuilder(String name) => - _$HxDatabaseBuilder(name); - - /// Creates a database builder for an in memory database. - /// Information stored in an in memory database disappears when the process is killed. - /// Once a database is built, you should keep a reference to it and re-use it. - static _$HxDatabaseBuilder inMemoryDatabaseBuilder() => - _$HxDatabaseBuilder(null); -} - -class _$HxDatabaseBuilder { - _$HxDatabaseBuilder(this.name); - - final String name; - - final List _migrations = []; - - Callback _callback; - - /// Adds migrations to the builder. - _$HxDatabaseBuilder addMigrations(List migrations) { - _migrations.addAll(migrations); - return this; - } - - /// Adds a database [Callback] to the builder. - _$HxDatabaseBuilder addCallback(Callback callback) { - _callback = callback; - return this; - } - - /// Creates the database and initializes it. - Future build() async { - final path = name != null - ? await sqfliteDatabaseFactory.getDatabasePath(name) - : ':memory:'; - final database = _$HxDatabase(); - database.database = await database.open( - path, - _migrations, - _callback, - ); - return database; - } -} - -class _$HxDatabase extends HxDatabase { - _$HxDatabase([StreamController listener]) { - changeListener = listener ?? StreamController.broadcast(); - } - - MessageDao _messageDaoInstance; - - Future open( - String path, - List migrations, [ - Callback callback, - ]) async { - final databaseOptions = sqflite.OpenDatabaseOptions( - version: 1, - onConfigure: (database) async { - await database.execute('PRAGMA foreign_keys = ON'); - await callback?.onConfigure?.call(database); - }, - onOpen: (database) async { - await callback?.onOpen?.call(database); - }, - onUpgrade: (database, startVersion, endVersion) async { - await MigrationAdapter.runMigrations( - database, startVersion, endVersion, migrations); - - await callback?.onUpgrade?.call(database, startVersion, endVersion); - }, - onCreate: (database, version) async { - await database.execute( - 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` INTEGER, `toId` INTEGER, `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` INTEGER, `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); - - await callback?.onCreate?.call(database, version); - }, - ); - return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); - } - - @override - MessageDao get messageDao { - return _messageDaoInstance ??= _$MessageDao(database, changeListener); - } -} - -class _$MessageDao extends MessageDao { - _$MessageDao( - this.database, - this.changeListener, - ) : _queryAdapter = QueryAdapter(database, changeListener), - _messageInsertionAdapter = InsertionAdapter( - database, - 'Message', - (Message item) => item.toJson(), - changeListener); - - final sqflite.DatabaseExecutor database; - - final StreamController changeListener; - - final QueryAdapter _queryAdapter; - - final InsertionAdapter _messageInsertionAdapter; - - @override - Stream> findMessageByToId(int toId) { - return _queryAdapter.queryListStream( - 'SELECT * FROM Message WHERE toId = ?1', - mapper: (Map row) => Message.fromJson(row), - arguments: [toId], - queryableName: 'Message', - isView: false); - } - - @override - Future> findMessageByGroup(int userId) { - debugPrint("findMessageByGroup: $userId"); - return _queryAdapter.queryList( - 'SELECT * FROM Message WHERE toId = ?1 OR fromId = ?2 GROUP BY toId,fromId ORDER BY time DESC', - mapper: (Map row) => Message.fromJson(row), - arguments: [userId, userId]); - } - - @override - Future insertMessage(Message message) async { - await _messageInsertionAdapter.insert(message, OnConflictStrategy.abort); - } -} +// // GENERATED CODE - DO NOT MODIFY BY HAND +// +// part of 'hx_database.dart'; +// +// // ************************************************************************** +// // FloorGenerator +// // ************************************************************************** +// +// // ignore: avoid_classes_with_only_static_members +// import 'package:floor/floor.dart'; +// +// class $FloorHxDatabase { +// /// Creates a database builder for a persistent database. +// /// Once a database is built, you should keep a reference to it and re-use it. +// static _$HxDatabaseBuilder databaseBuilder(String name) => +// _$HxDatabaseBuilder(name); +// +// /// Creates a database builder for an in memory database. +// /// Information stored in an in memory database disappears when the process is killed. +// /// Once a database is built, you should keep a reference to it and re-use it. +// static _$HxDatabaseBuilder inMemoryDatabaseBuilder() => +// _$HxDatabaseBuilder(null); +// } +// +// class _$HxDatabaseBuilder { +// _$HxDatabaseBuilder(this.name); +// +// final String name; +// +// final List _migrations = []; +// +// Callback _callback; +// +// /// Adds migrations to the builder. +// _$HxDatabaseBuilder addMigrations(List migrations) { +// _migrations.addAll(migrations); +// return this; +// } +// +// /// Adds a database [Callback] to the builder. +// _$HxDatabaseBuilder addCallback(Callback callback) { +// _callback = callback; +// return this; +// } +// +// /// Creates the database and initializes it. +// Future build() async { +// final path = name != null +// ? await sqfliteDatabaseFactory.getDatabasePath(name) +// : ':memory:'; +// final database = _$HxDatabase(); +// database.database = await database.open( +// path, +// _migrations, +// _callback, +// ); +// return database; +// } +// } +// +// class _$HxDatabase extends HxDatabase { +// _$HxDatabase([StreamController listener]) { +// changeListener = listener ?? StreamController.broadcast(); +// } +// +// MessageDao _messageDaoInstance; +// +// Future open( +// String path, +// List migrations, [ +// Callback callback, +// ]) async { +// final databaseOptions = sqflite.OpenDatabaseOptions( +// version: 1, +// onConfigure: (database) async { +// await database.execute('PRAGMA foreign_keys = ON'); +// await callback?.onConfigure?.call(database); +// }, +// onOpen: (database) async { +// await callback?.onOpen?.call(database); +// }, +// onUpgrade: (database, startVersion, endVersion) async { +// await MigrationAdapter.runMigrations( +// database, startVersion, endVersion, migrations); +// +// await callback?.onUpgrade?.call(database, startVersion, endVersion); +// }, +// onCreate: (database, version) async { +// await database.execute( +// 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` INTEGER, `toId` INTEGER, `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` INTEGER, `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); +// +// await callback?.onCreate?.call(database, version); +// }, +// ); +// return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); +// } +// +// @override +// MessageDao get messageDao { +// return _messageDaoInstance ??= _$MessageDao(database, changeListener); +// } +// } +// +// class _$MessageDao extends MessageDao { +// _$MessageDao( +// this.database, +// this.changeListener, +// ) : _queryAdapter = QueryAdapter(database, changeListener), +// _messageInsertionAdapter = InsertionAdapter( +// database, +// 'Message', +// (Message item) => item.toJson(), +// changeListener); +// +// final sqflite.DatabaseExecutor database; +// +// final StreamController changeListener; +// +// final QueryAdapter _queryAdapter; +// +// final InsertionAdapter _messageInsertionAdapter; +// +// @override +// Stream> findMessageByToId(int toId) { +// return _queryAdapter.queryListStream( +// 'SELECT * FROM Message WHERE toId = ?1', +// mapper: (Map row) => Message.fromJson(row), +// arguments: [toId], +// queryableName: 'Message', +// isView: false); +// } +// +// @override +// Future> findMessageByGroup(int userId) { +// debugPrint("findMessageByGroup: $userId"); +// return _queryAdapter.queryList( +// 'SELECT * FROM Message WHERE toId = ?1 OR fromId = ?2 GROUP BY toId,fromId ORDER BY time DESC', +// mapper: (Map row) => Message.fromJson(row), +// arguments: [userId, userId]); +// } +// +// @override +// Future insertMessage(Message message) async { +// await _messageInsertionAdapter.insert(message, OnConflictStrategy.abort); +// } +// } diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index cc16be6f..741f8778 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -1,13 +1,12 @@ -import 'package:floor/floor.dart'; -@entity class Message { - @primaryKey int id; - int fromId; + String fromId; - int toId; + String toId; + + String replyId; String content; @@ -15,18 +14,19 @@ class Message { int msgType; - int time; + String time; int state; int isDelete; - Message(id, fromId, toId, content, attach, msgType, time, state, isDelete); + Message(this.id, this.fromId, this.toId, this.replyId, this.content, this.attach, this.msgType, this.time, this.state, this.isDelete); factory Message.fromJson(Map json) => Message( json["id"], json["fromId"], json["toId"], + json["replyId"], json["content"], json["attach"], json["msgType"], @@ -38,6 +38,7 @@ class Message { "id": id, "fromId": fromId, "toId": toId, + "replyId": replyId, "content": content, "attach": attach, "msgType": msgType, @@ -47,15 +48,16 @@ class Message { }; } -createMessage(var toId, String content, {String attach, int msgType, userId}) { - return Message.fromJson({ +createMessage(var toId, String content, {String attach, int msgType, userId, replyId}) { + return { "fromId": userId, "toId": toId, + "replyId": replyId, "content": content, "attach": attach, "msgType": msgType ?? 0, - "time": DateTime.now().millisecondsSinceEpoch, + "time": "${DateTime.now().millisecondsSinceEpoch}", "state": 0, "isDelete": 0 - }); + }; } diff --git a/lib/im/database/message_dao.dart b/lib/im/database/message_dao.dart index 0946b741..a2b59ce9 100644 --- a/lib/im/database/message_dao.dart +++ b/lib/im/database/message_dao.dart @@ -1,17 +1,17 @@ -import 'package:floor/floor.dart'; -import 'package:huixiang/im/database/message.dart'; - - -@dao -abstract class MessageDao { - - @Query('SELECT * FROM Message WHERE toId = :toId') - Stream> findMessageByToId(int toId); - - @insert - Future insertMessage(Message message); - - @Query('SELECT * FROM Message WHERE toId = :userId OR fromId = :userId GROUP BY toId,fromId ORDER BY time DESC') - Future> findMessageByGroup(int userId); - -} \ No newline at end of file +// import 'package:floor/floor.dart'; +// import 'package:huixiang/im/database/message.dart'; +// +// +// @dao +// abstract class MessageDao { +// +// @Query('SELECT * FROM Message WHERE toId = :toId') +// Stream> findMessageByToId(int toId); +// +// @insert +// Future insertMessage(Message message); +// +// @Query('SELECT * FROM Message WHERE toId = :userId OR fromId = :userId GROUP BY toId,fromId ORDER BY time DESC') +// Future> findMessageByGroup(int userId); +// +// } \ No newline at end of file diff --git a/lib/im/database/migration.dart b/lib/im/database/migration.dart new file mode 100644 index 00000000..9585b2f9 --- /dev/null +++ b/lib/im/database/migration.dart @@ -0,0 +1,41 @@ +import 'package:sqflite/sqflite.dart' as sqflite; + +/// Base class for a database migration. +/// +/// Each migration can move between 2 versions that are defined by +/// [startVersion] and [endVersion]. +class Migration { + /// The start version of the database. + final int startVersion; + + /// The start version of the database. + final int endVersion; + + /// Function that performs the migration. + final Future Function(sqflite.Database database) migrate; + + /// Creates a new migration between [startVersion] and [endVersion]. + /// [migrate] will be called by the database and performs the actual + /// migration. + Migration(this.startVersion, this.endVersion, this.migrate) + : assert(startVersion > 0), + assert(startVersion < endVersion); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Migration && + runtimeType == other.runtimeType && + startVersion == other.startVersion && + endVersion == other.endVersion && + migrate == other.migrate; + + @override + int get hashCode => + startVersion.hashCode ^ endVersion.hashCode ^ migrate.hashCode; + + @override + String toString() { + return 'Migration{startVersion: $startVersion, endVersion: $endVersion, migrate: $migrate}'; + } +} diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 18805a7a..c346a458 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -74,29 +74,20 @@ class _IMPage extends State implements OnChatMessage { queryMsgStats(); } - List userIds = []; + List userIds = []; Stream streamSubscription ; loadMessageList() async { - int userId = 123456; - - hxDatabase.changeListener.stream.listen((event) { - debugPrint("messages: 1111"); - }, onError: (Object error, stackTrace) { - debugPrint("messages: 3333"); - }); - hxDatabase.changeListener.onListen = () { - debugPrint("messages: 2222"); - }; - - messages = await hxDatabase.messageDao.findMessageByGroup(userId); + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); + messages = await hxDatabase.queryList(userId); messages.forEach((element) { - debugPrint("messages: $element"); + debugPrint("messages: ${element.toJson()}"); }); userIds = messages .map((e) => e.toId != userId ? e.toId : e.fromId) - .toSet() + .toSet().where((element) => element != userId) .toList(); if (mounted) { setState(() {}); @@ -289,7 +280,6 @@ class _IMPage extends State implements OnChatMessage { child: TextField( textInputAction: TextInputAction.search, onEditingComplete: () { - socketClient.sendMessage(654321, "hello~"); FocusScope.of(context).requestFocus(FocusNode()); }, controller: imEditingController, @@ -331,7 +321,12 @@ class _IMPage extends State implements OnChatMessage { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Navigator.of(context).pushNamed('/router/chat_details_page'); + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toId": userIds[position], + }, + ); }, child: chatItem(userIds[position]), ); @@ -370,8 +365,8 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - "喽哈$userId", - overflow: TextOverflow.ellipsis, + "喽哈 $userId", + overflow: TextOverflow.fade, maxLines: 1, style: TextStyle( fontSize: 16.sp, diff --git a/lib/im/out/auth.pb.dart b/lib/im/out/auth.pb.dart index d4efd840..8296a40c 100644 --- a/lib/im/out/auth.pb.dart +++ b/lib/im/out/auth.pb.dart @@ -15,7 +15,7 @@ import 'package:protobuf/protobuf.dart' as $pb; class AuthReq extends $pb.GeneratedMessage { factory AuthReq({ - $core.int? uid, + $core.String? uid, $core.String? token, }) { final $result = create(); @@ -32,7 +32,7 @@ class AuthReq extends $pb.GeneratedMessage { factory AuthReq.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthReq', createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'uid', $pb.PbFieldType.OU3) + ..aOS(1, _omitFieldNames ? '' : 'uid') ..aOS(2, _omitFieldNames ? '' : 'token') ..hasRequiredFields = false ; @@ -59,9 +59,9 @@ class AuthReq extends $pb.GeneratedMessage { static AuthReq? _defaultInstance; @$pb.TagNumber(1) - $core.int get uid => $_getIZ(0); + $core.String get uid => $_getSZ(0); @$pb.TagNumber(1) - set uid($core.int v) { $_setUnsignedInt32(0, v); } + set uid($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasUid() => $_has(0); @$pb.TagNumber(1) @@ -79,7 +79,7 @@ class AuthReq extends $pb.GeneratedMessage { class AuthResp extends $pb.GeneratedMessage { factory AuthResp({ - $core.int? uid, + $core.String? uid, $core.int? code, $core.String? message, }) { @@ -100,7 +100,7 @@ class AuthResp extends $pb.GeneratedMessage { factory AuthResp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthResp', createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'uid', $pb.PbFieldType.OU3) + ..aOS(1, _omitFieldNames ? '' : 'uid') ..a<$core.int>(2, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) ..aOS(3, _omitFieldNames ? '' : 'message') ..hasRequiredFields = false @@ -128,9 +128,9 @@ class AuthResp extends $pb.GeneratedMessage { static AuthResp? _defaultInstance; @$pb.TagNumber(1) - $core.int get uid => $_getIZ(0); + $core.String get uid => $_getSZ(0); @$pb.TagNumber(1) - set uid($core.int v) { $_setUnsignedInt32(0, v); } + set uid($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasUid() => $_has(0); @$pb.TagNumber(1) diff --git a/lib/im/out/auth.pbjson.dart b/lib/im/out/auth.pbjson.dart index d97595c5..266977c2 100644 --- a/lib/im/out/auth.pbjson.dart +++ b/lib/im/out/auth.pbjson.dart @@ -17,20 +17,20 @@ import 'dart:typed_data' as $typed_data; const AuthReq$json = { '1': 'AuthReq', '2': [ - {'1': 'uid', '3': 1, '4': 1, '5': 13, '10': 'uid'}, + {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'}, {'1': 'token', '3': 2, '4': 1, '5': 9, '10': 'token'}, ], }; /// Descriptor for `AuthReq`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authReqDescriptor = $convert.base64Decode( - 'CgdBdXRoUmVxEhAKA3VpZBgBIAEoDVIDdWlkEhQKBXRva2VuGAIgASgJUgV0b2tlbg=='); + 'CgdBdXRoUmVxEhAKA3VpZBgBIAEoCVIDdWlkEhQKBXRva2VuGAIgASgJUgV0b2tlbg=='); @$core.Deprecated('Use authRespDescriptor instead') const AuthResp$json = { '1': 'AuthResp', '2': [ - {'1': 'uid', '3': 1, '4': 1, '5': 13, '10': 'uid'}, + {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'}, {'1': 'code', '3': 2, '4': 1, '5': 13, '10': 'code'}, {'1': 'message', '3': 3, '4': 1, '5': 9, '10': 'message'}, ], @@ -38,6 +38,6 @@ const AuthResp$json = { /// Descriptor for `AuthResp`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authRespDescriptor = $convert.base64Decode( - 'CghBdXRoUmVzcBIQCgN1aWQYASABKA1SA3VpZBISCgRjb2RlGAIgASgNUgRjb2RlEhgKB21lc3' + 'CghBdXRoUmVzcBIQCgN1aWQYASABKAlSA3VpZBISCgRjb2RlGAIgASgNUgRjb2RlEhgKB21lc3' 'NhZ2UYAyABKAlSB21lc3NhZ2U='); diff --git a/lib/im/out/message.pb.dart b/lib/im/out/message.pb.dart index 360aae2a..c11e7f1b 100644 --- a/lib/im/out/message.pb.dart +++ b/lib/im/out/message.pb.dart @@ -19,8 +19,8 @@ export 'message.pbenum.dart'; class MsgData extends $pb.GeneratedMessage { factory MsgData({ - $core.int? to, - $core.int? from, + $core.String? to, + $core.String? from, $core.int? ctime, MsgType? type, $core.List<$core.int>? data, @@ -48,8 +48,8 @@ class MsgData extends $pb.GeneratedMessage { factory MsgData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MsgData', createEmptyInstance: create) - ..a<$core.int>(1, _omitFieldNames ? '' : 'to', $pb.PbFieldType.OU3) - ..a<$core.int>(2, _omitFieldNames ? '' : 'from', $pb.PbFieldType.OU3) + ..aOS(1, _omitFieldNames ? '' : 'to') + ..aOS(2, _omitFieldNames ? '' : 'from') ..a<$core.int>(3, _omitFieldNames ? '' : 'ctime', $pb.PbFieldType.OU3) ..e(4, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MsgType.SINGLE_TEXT, valueOf: MsgType.valueOf, enumValues: MsgType.values) ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) @@ -78,18 +78,18 @@ class MsgData extends $pb.GeneratedMessage { static MsgData? _defaultInstance; @$pb.TagNumber(1) - $core.int get to => $_getIZ(0); + $core.String get to => $_getSZ(0); @$pb.TagNumber(1) - set to($core.int v) { $_setUnsignedInt32(0, v); } + set to($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasTo() => $_has(0); @$pb.TagNumber(1) void clearTo() => clearField(1); @$pb.TagNumber(2) - $core.int get from => $_getIZ(1); + $core.String get from => $_getSZ(1); @$pb.TagNumber(2) - set from($core.int v) { $_setUnsignedInt32(1, v); } + set from($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasFrom() => $_has(1); @$pb.TagNumber(2) diff --git a/lib/im/out/message.pbjson.dart b/lib/im/out/message.pbjson.dart index 1870d86d..28ae82b4 100644 --- a/lib/im/out/message.pbjson.dart +++ b/lib/im/out/message.pbjson.dart @@ -33,8 +33,8 @@ final $typed_data.Uint8List msgTypeDescriptor = $convert.base64Decode( const MsgData$json = { '1': 'MsgData', '2': [ - {'1': 'to', '3': 1, '4': 1, '5': 13, '10': 'to'}, - {'1': 'from', '3': 2, '4': 1, '5': 13, '10': 'from'}, + {'1': 'to', '3': 1, '4': 1, '5': 9, '10': 'to'}, + {'1': 'from', '3': 2, '4': 1, '5': 9, '10': 'from'}, {'1': 'ctime', '3': 3, '4': 1, '5': 13, '10': 'ctime'}, {'1': 'type', '3': 4, '4': 1, '5': 14, '6': '.MsgType', '10': 'type'}, {'1': 'data', '3': 5, '4': 1, '5': 12, '10': 'data'}, @@ -43,7 +43,7 @@ const MsgData$json = { /// Descriptor for `MsgData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List msgDataDescriptor = $convert.base64Decode( - 'CgdNc2dEYXRhEg4KAnRvGAEgASgNUgJ0bxISCgRmcm9tGAIgASgNUgRmcm9tEhQKBWN0aW1lGA' + 'CgdNc2dEYXRhEg4KAnRvGAEgASgJUgJ0bxISCgRmcm9tGAIgASgJUgRmcm9tEhQKBWN0aW1lGA' 'MgASgNUgVjdGltZRIcCgR0eXBlGAQgASgOMgguTXNnVHlwZVIEdHlwZRISCgRkYXRhGAUgASgM' 'UgRkYXRh'); diff --git a/lib/main.dart b/lib/main.dart index af51ef2f..6fa68f1c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -221,7 +221,8 @@ void main() async { HxDatabase hxDatabase; initDatabase() async { - hxDatabase = await $FloorHxDatabase.databaseBuilder('huixiang_database.db').build(); + hxDatabase = HxDatabase(); + await hxDatabase.open(); } final SocketClient socketClient = new SocketClient(); @@ -505,7 +506,7 @@ Map routers = { '/router/contacts_share': (context, {arguments}) => ContactsShare(arguments:arguments), '/router/chat_details_page': (context, {arguments}) => - ChatDetailsPage(), + ChatDetailsPage(arguments: arguments), '/router/chat_setting': (context, {arguments}) => ChatSetting(), '/router/chat_friend_group': (context, {arguments}) => diff --git a/lib/mine/edit_signature.dart b/lib/mine/edit_signature.dart index 19be0aeb..38431bb0 100644 --- a/lib/mine/edit_signature.dart +++ b/lib/mine/edit_signature.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/generated/l10n.dart'; -import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; diff --git a/pubspec.lock b/pubspec.lock index 8c9cac60..e107bfab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -265,22 +265,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" - floor: - dependency: "direct main" - description: - name: floor - sha256: "52a8eac2c8d274e7c0c54251226f59786bb5b749365a2d8537d8095aa5132d92" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.2" - floor_annotation: - dependency: transitive - description: - name: floor_annotation - sha256: fa3fa4f198cdd1d922a69ceb06e54663fe59256bf1cb3c036eff206b445a6960 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.2" flutter: dependency: "direct main" description: flutter @@ -955,7 +939,7 @@ packages: source: hosted version: "1.9.1" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 @@ -970,30 +954,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.5+1" - sqflite_common_ffi: - dependency: transitive - description: - name: sqflite_common_ffi - sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.5" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "281b672749af2edf259fc801f0fcba092257425bcd32a0ce1c8237130bc934c7" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.11.2" - sqlparser: - dependency: transitive - description: - name: sqlparser - sha256: "91f47610aa54d8abf9d795a7b4e49b2a788f65d7493d5a68fbf180c3cbcc6f38" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.27.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ade313dd..e3145f3a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,7 +123,8 @@ dependencies: flutter_datetime_picker: ^1.5.1 widgetpicker: ^0.1.1 - floor: ^1.4.2 +# floor: ^1.4.2 + sqflite: ^2.2.2 syncfusion_flutter_datepicker: ^19.4.38 protobuf: ^3.1.0 From f42ad0cfaa78035f13e77678be315761b413086d Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Wed, 18 Sep 2024 10:34:55 +0800 Subject: [PATCH 02/81] =?UTF-8?q?tab=E6=96=B0=E5=A2=9E=E4=B8=8B=E5=88=92?= =?UTF-8?q?=E7=BA=BF=E5=9C=86=E8=A7=92=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/custom_underline_tabIndicator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/im_view/custom_underline_tabIndicator.dart b/lib/im/im_view/custom_underline_tabIndicator.dart index 0a5f3349..0a02c576 100644 --- a/lib/im/im_view/custom_underline_tabIndicator.dart +++ b/lib/im/im_view/custom_underline_tabIndicator.dart @@ -89,7 +89,7 @@ class _UnderlinePainter extends BoxPainter { final Rect indicator = decoration._indicatorRectFor(rect, textDirection) .deflate(decoration.borderSide.width / 2.0); final Paint paint = decoration.borderSide.toPaint(); - paint.strokeWidth = 5; + paint.strokeWidth = 4; paint.strokeCap = StrokeCap.round; //主要是修改此处 圆角 canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint); } From 32bba1b888adbe6a87bdda364fb16778ef15b4db Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 18 Sep 2024 15:28:09 +0800 Subject: [PATCH 03/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 160108c4..f8b11346 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -20,7 +20,7 @@ class SocketClient { connect() async { shared = await SharedPreferences.getInstance(); - await Socket.connect('192.168.10.129', 9090).then((value) { + await Socket.connect('192.168.10.200', 49168).then((value) { debugPrint("socket-connect"); _socket = value; _socket.listen((data) { @@ -32,29 +32,32 @@ class SocketClient { hxDatabase.insert(createMessage(userId, utf8.decode(data1.data), msgType: data1.type.value, userId: data1.from)); - callbacks.forEach((callback) { + callbacks.values.forEach((callback) { callback.call(data1); }); }, onError: (Object error, StackTrace stackTrace) { - debugPrint("socket-error: $error, stackTrace: ${stackTrace}"); + debugPrint("socket-listen-error: $error, stackTrace: ${stackTrace}"); }); authRequest(shared.getString("token")); }).catchError((error) { debugPrint("socket-connect-error: $error"); + Future.delayed(const Duration(milliseconds: 3000), () { + connect(); + }); }); } - List callbacks = []; + Map callbacks = {}; - addCallback(Function callback) { - callbacks.add(callback); + addCallback(String userId, Function callback) { + callbacks.putIfAbsent(userId, callback); } - removeCallback(Function callback) { - callbacks.remove(callback); + removeCallback(String userId) { + callbacks.remove(userId); } dispose() { From 759bd9a31dfce085fa4e38f4eeb9ba09cb44f74a Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Wed, 18 Sep 2024 16:19:43 +0800 Subject: [PATCH 04/81] =?UTF-8?q?out=E5=8E=8B=E7=BC=A9=E5=8C=85=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=EF=BC=9B=20=E6=96=B0=E5=A2=9E=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3=EF=BC=9B=20=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A1=B5=E9=9D=A2=E6=9B=B4=E6=94=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 2 +- lib/im/chat_details_page.dart | 1 - lib/im/im_view/im_page.dart | 2 +- lib/im/out/message.pb.dart | 2 +- lib/im/out/message.pbenum.dart | 24 +- lib/im/out/message.pbjson.dart | 16 +- lib/mine/personal_page.dart | 1059 ++++++++++++++++++++++-------- lib/retrofit/retrofit_api.dart | 5 + lib/retrofit/retrofit_api.g.dart | 26 + 9 files changed, 836 insertions(+), 301 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 160108c4..353c7b6a 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -89,7 +89,7 @@ class SocketClient { } message["id"] = id; Uint8List data = utf8.encode(content); - MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.SINGLE_TEXT, data: data); + MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.COMMAND, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); _socket.add(proto2.toBytes()); debugPrint("sendMessage: ${message["id"]}"); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 42e5c231..049fdeb4 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index c346a458..12ccec6e 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -366,7 +366,7 @@ class _IMPage extends State implements OnChatMessage { Expanded( child: Text( "喽哈 $userId", - overflow: TextOverflow.fade, + // overflow: TextOverflow.fade, maxLines: 1, style: TextStyle( fontSize: 16.sp, diff --git a/lib/im/out/message.pb.dart b/lib/im/out/message.pb.dart index c11e7f1b..42e143de 100644 --- a/lib/im/out/message.pb.dart +++ b/lib/im/out/message.pb.dart @@ -51,7 +51,7 @@ class MsgData extends $pb.GeneratedMessage { ..aOS(1, _omitFieldNames ? '' : 'to') ..aOS(2, _omitFieldNames ? '' : 'from') ..a<$core.int>(3, _omitFieldNames ? '' : 'ctime', $pb.PbFieldType.OU3) - ..e(4, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MsgType.SINGLE_TEXT, valueOf: MsgType.valueOf, enumValues: MsgType.values) + ..e(4, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MsgType.COMMAND, valueOf: MsgType.valueOf, enumValues: MsgType.values) ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) ..hasRequiredFields = false ; diff --git a/lib/im/out/message.pbenum.dart b/lib/im/out/message.pbenum.dart index a78564c4..df67c4a9 100644 --- a/lib/im/out/message.pbenum.dart +++ b/lib/im/out/message.pbenum.dart @@ -14,16 +14,24 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class MsgType extends $pb.ProtobufEnum { - static const MsgType SINGLE_TEXT = MsgType._(0, _omitEnumNames ? '' : 'SINGLE_TEXT'); - static const MsgType SINGLE_AUDIO = MsgType._(1, _omitEnumNames ? '' : 'SINGLE_AUDIO'); - static const MsgType GROUP_TEXT = MsgType._(2, _omitEnumNames ? '' : 'GROUP_TEXT'); - static const MsgType GROUP_AUDIO = MsgType._(3, _omitEnumNames ? '' : 'GROUP_AUDIO'); + static const MsgType COMMAND = MsgType._(0, _omitEnumNames ? '' : 'COMMAND'); + static const MsgType TEXT = MsgType._(1, _omitEnumNames ? '' : 'TEXT'); + static const MsgType IMAGE = MsgType._(2, _omitEnumNames ? '' : 'IMAGE'); + static const MsgType AUDIO = MsgType._(3, _omitEnumNames ? '' : 'AUDIO'); + static const MsgType VIDEO = MsgType._(4, _omitEnumNames ? '' : 'VIDEO'); + static const MsgType PACKET = MsgType._(5, _omitEnumNames ? '' : 'PACKET'); + static const MsgType TRANSFER = MsgType._(6, _omitEnumNames ? '' : 'TRANSFER'); + static const MsgType LOCATION = MsgType._(7, _omitEnumNames ? '' : 'LOCATION'); static const $core.List values = [ - SINGLE_TEXT, - SINGLE_AUDIO, - GROUP_TEXT, - GROUP_AUDIO, + COMMAND, + TEXT, + IMAGE, + AUDIO, + VIDEO, + PACKET, + TRANSFER, + LOCATION, ]; static final $core.Map<$core.int, MsgType> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/im/out/message.pbjson.dart b/lib/im/out/message.pbjson.dart index 28ae82b4..bd7dd1be 100644 --- a/lib/im/out/message.pbjson.dart +++ b/lib/im/out/message.pbjson.dart @@ -17,17 +17,21 @@ import 'dart:typed_data' as $typed_data; const MsgType$json = { '1': 'MsgType', '2': [ - {'1': 'SINGLE_TEXT', '2': 0}, - {'1': 'SINGLE_AUDIO', '2': 1}, - {'1': 'GROUP_TEXT', '2': 2}, - {'1': 'GROUP_AUDIO', '2': 3}, + {'1': 'COMMAND', '2': 0}, + {'1': 'TEXT', '2': 1}, + {'1': 'IMAGE', '2': 2}, + {'1': 'AUDIO', '2': 3}, + {'1': 'VIDEO', '2': 4}, + {'1': 'PACKET', '2': 5}, + {'1': 'TRANSFER', '2': 6}, + {'1': 'LOCATION', '2': 7}, ], }; /// Descriptor for `MsgType`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List msgTypeDescriptor = $convert.base64Decode( - 'CgdNc2dUeXBlEg8KC1NJTkdMRV9URVhUEAASEAoMU0lOR0xFX0FVRElPEAESDgoKR1JPVVBfVE' - 'VYVBACEg8KC0dST1VQX0FVRElPEAM='); + 'CgdNc2dUeXBlEgsKB0NPTU1BTkQQABIICgRURVhUEAESCQoFSU1BR0UQAhIJCgVBVURJTxADEg' + 'kKBVZJREVPEAQSCgoGUEFDS0VUEAUSDAoIVFJBTlNGRVIQBhIMCghMT0NBVElPThAH'); @$core.Deprecated('Use msgDataDescriptor instead') const MsgData$json = { diff --git a/lib/mine/personal_page.dart b/lib/mine/personal_page.dart index 0b478bb2..2202e9a6 100644 --- a/lib/mine/personal_page.dart +++ b/lib/mine/personal_page.dart @@ -26,6 +26,8 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../view_widget/my_tab.dart'; + class PersonalPage extends StatefulWidget { final Map arguments; @@ -37,7 +39,7 @@ class PersonalPage extends StatefulWidget { } } -class _PersonalPage extends State with WidgetsBindingObserver { +class _PersonalPage extends State with WidgetsBindingObserver,SingleTickerProviderStateMixin{ ApiService apiService; final RefreshController refreshController = RefreshController(); final ScrollController scrollController = ScrollController(); @@ -52,6 +54,7 @@ class _PersonalPage extends State with WidgetsBindingObserver { bool isLoadingData = false; String memberId; Map modifyInfo = {"background": ""}; + TabController tabController; @override void initState() { @@ -370,297 +373,80 @@ class _PersonalPage extends State with WidgetsBindingObserver { mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - height: 248.h, - color: Color(0xFFFFFFFF), - // color: Colors.red, - child: Stack( - // alignment: Alignment.bottomLeft, - children: [ + Stack( + children: [ + Container( + color: Colors.black, + child: Opacity( + opacity: 0.9, + child: MImage( + memberInfor?.background ?? "", + width: double.infinity, + height: 210.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + ))), + Positioned( + top: MediaQuery.of(context).padding.top + 20.h, + left: 17.w, + child:GestureDetector( + child: Image.asset( + "assets/image/integral_return.webp", + width: 24, + height: 24, + ), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + if (memberId == "0") Positioned( - top: 0, - left: 0, - bottom: 36.h, - right: 0, - child: Stack( - alignment: Alignment.bottomRight, - children: [ - Container( - color: Colors.black, - child: Opacity( - opacity: 0.9, - child: MImage( - memberInfor?.background ?? "", - width: double.infinity, - height: 260.h, - fit: BoxFit.cover, - errorSrc: "assets/image/default_1.webp", - fadeSrc: "assets/image/default_1.webp", - ))), - Container( - margin: EdgeInsets.only( - top: 50.h, left: 16.w, right: 16.w), - alignment: Alignment.topLeft, - decoration: BoxDecoration( - color: Colors.transparent, - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - child: Image.asset( - "assets/image/integral_return.webp", - width: 24, - height: 24, - ), - onTap: () { - Navigator.of(context).pop(); - }, - ), - ], - ), + bottom: 9.h, + right: 16.w, + child: GestureDetector( + onTap: () { + showImagePicker(); + }, + child: Container( + padding: + EdgeInsets.only(left: 2, right: 2), + width: 59.w, + height: 23.h, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: Color(0x80000000), ), - if (memberId == "0") - Positioned( - bottom: 9.h, - right: 16.w, - child: GestureDetector( - onTap: () { - showImagePicker(); - }, - child: Container( - padding: - EdgeInsets.only(left: 2, right: 2), - width: 59.w, - height: 23.h, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2), - color: Color(0x80000000), - ), - child: Text( - S.of(context).genghuanbeijing, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - color: Color(0xFFFFFFFF), - ), - ), - ), - ), + child: Text( + S.of(context).genghuanbeijing, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + color: Color(0xFFFFFFFF), ), - ], + ), + ), ), ), - homeInfo(), - ], - ), + ], ), Container( - color: Color(0xFFFFFFFF), - margin: EdgeInsets.only(bottom: 30.h), + // margin: EdgeInsets.only(bottom: 30.h), + decoration: BoxDecoration( + color: Color(0xFFFFFFFF), + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + topLeft: Radius.circular(12)), + ), child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - margin: EdgeInsets.only( - left: 86.w, top: 12.h, right: 16.w), - child: GestureDetector( - onTap: () { - setState(() { - isShrink = !isShrink; - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - flex: 1, - child: Text( - memberId == "0" - ? ((memberInfor?.signature == "") - ? "还未编辑个性签名~" - : memberInfor?.signature ?? "") - : "个性签名: ${(memberInfor?.signature == "") ? "还未编辑个性签名~" : memberInfor?.signature ?? ""}", - overflow: isShrink - ? TextOverflow.visible - : TextOverflow.ellipsis, - maxLines: isShrink ? 10 : 2, - style: TextStyle( - fontSize: 12.sp, - color: Color(0xFF868686), - fontWeight: MyFontWeight.regular, - height: 1.5.h), - )), - Icon( - (isShrink != null && !isShrink) - ? Icons.keyboard_arrow_down - : Icons.keyboard_arrow_up, - color: Colors.black, - size: 18, - ), - ], - ), - )), - Container( - margin: EdgeInsets.only(left: 16.w, right: 16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.all(16), - child: Column( - children: [ - Text( - memberInfor != null - ? memberInfor.follow.toString() - : "0", - style: TextStyle( - color: Color(0xFF353535), - fontSize: 16.sp, - fontWeight: - MyFontWeight.semi_bold, - ), - ), - SizedBox( - height: 4.h, - ), - Text( - S.of(context).guanzhu, - style: TextStyle( - color: Color(0xFF353535), - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - )), - ), - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.all(16), - child: Column( - children: [ - Text( - memberInfor != null - ? memberInfor.fans.toString() - : "0", - style: TextStyle( - color: Color(0xFF353535), - fontSize: 16.sp, - fontWeight: - MyFontWeight.semi_bold, - ), - ), - SizedBox( - height: 4.h, - ), - Text( - S.of(context).fensi, - style: TextStyle( - color: Color(0xFF353535), - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - )), - ), - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.all(16), - child: Column( - children: [ - Text( - (memberInfor?.trendTotal ?? 0) - .toString(), - style: TextStyle( - color: Color(0xFF353535), - fontSize: 16.sp, - fontWeight: - MyFontWeight.semi_bold, - ), - ), - SizedBox( - height: 4.h, - ), - Text( - S.of(context).dongtai, - style: TextStyle( - color: Color(0xFF353535), - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - )), - ), - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.all(16), - child: Column( - children: [ - Text( - (memberInfor?.gainLikeTotal ?? 0) - .toString(), - style: TextStyle( - color: Color(0xFF353535), - fontSize: 16.sp, - fontWeight: - MyFontWeight.semi_bold, - ), - ), - SizedBox( - height: 4.h, - ), - Text( - S.of(context).huozan, - style: TextStyle( - color: Color(0xFF353535), - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - )), - ), - ), - ], - ), - ), - Container( - margin: EdgeInsets.all(16), - height: 1.h, - color: Color(0xFFF7F7F7), - ), + trendsInfo(), Padding( - padding: EdgeInsets.only(left: 16.w), + padding: EdgeInsets.only(left: 16.w,top:24.h), child: Text( memberId != "0" ? "TA的动态" : "我的动态", style: TextStyle( @@ -673,11 +459,315 @@ class _PersonalPage extends State with WidgetsBindingObserver { ], ), ), + // Container( + // height: 248.h, + // // color: Color(0xFFFFFFFF), + // // color: Colors.red, + // child: Stack( + // // alignment: Alignment.bottomLeft, + // children: [ + // Positioned( + // top: 0, + // left: 0, + // bottom: 36.h, + // right: 0, + // child: Stack( + // alignment: Alignment.bottomRight, + // children: [ + // Container( + // color: Colors.black, + // child: Opacity( + // opacity: 0.9, + // child: MImage( + // memberInfor?.background ?? "", + // width: double.infinity, + // height: 260.h, + // fit: BoxFit.cover, + // errorSrc: "assets/image/default_1.webp", + // fadeSrc: "assets/image/default_1.webp", + // ))), + // Container( + // margin: EdgeInsets.only( + // top: 50.h, left: 16.w, right: 16.w), + // alignment: Alignment.topLeft, + // decoration: BoxDecoration( + // color: Colors.transparent, + // ), + // child: Column( + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // GestureDetector( + // child: Image.asset( + // "assets/image/integral_return.webp", + // width: 24, + // height: 24, + // ), + // onTap: () { + // Navigator.of(context).pop(); + // }, + // ), + // ], + // ), + // ), + // if (memberId == "0") + // Positioned( + // bottom: 9.h, + // right: 16.w, + // child: GestureDetector( + // onTap: () { + // showImagePicker(); + // }, + // child: Container( + // padding: + // EdgeInsets.only(left: 2, right: 2), + // width: 59.w, + // height: 23.h, + // alignment: Alignment.center, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(2), + // color: Color(0x80000000), + // ), + // child: Text( + // S.of(context).genghuanbeijing, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // color: Color(0xFFFFFFFF), + // ), + // ), + // ), + // ), + // ), + // ], + // ), + // ), + // homeInfo(), + // ], + // ), + // ), + // Container( + // color: Color(0xFFFFFFFF), + // margin: EdgeInsets.only(bottom: 30.h), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.spaceAround, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Container( + // margin: EdgeInsets.only( + // left: 86.w, top: 12.h, right: 16.w), + // child: GestureDetector( + // onTap: () { + // setState(() { + // isShrink = !isShrink; + // }); + // }, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.end, + // children: [ + // Expanded( + // flex: 1, + // child: Text( + // memberId == "0" + // ? ((memberInfor?.signature == "") + // ? "还未编辑个性签名~" + // : memberInfor?.signature ?? "") + // : "个性签名: ${(memberInfor?.signature == "") ? "还未编辑个性签名~" : memberInfor?.signature ?? ""}", + // overflow: isShrink + // ? TextOverflow.visible + // : TextOverflow.ellipsis, + // maxLines: isShrink ? 10 : 2, + // style: TextStyle( + // fontSize: 12.sp, + // color: Color(0xFF868686), + // fontWeight: MyFontWeight.regular, + // height: 1.5.h), + // )), + // Icon( + // (isShrink != null && !isShrink) + // ? Icons.keyboard_arrow_down + // : Icons.keyboard_arrow_up, + // color: Colors.black, + // size: 18, + // ), + // ], + // ), + // )), + // Container( + // margin: EdgeInsets.only(left: 16.w, right: 16.w), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceAround, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Expanded( + // child: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: () {}, + // child: Container( + // color: Colors.transparent, + // padding: EdgeInsets.all(16), + // child: Column( + // children: [ + // Text( + // memberInfor != null + // ? memberInfor.follow.toString() + // : "0", + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 16.sp, + // fontWeight: + // MyFontWeight.semi_bold, + // ), + // ), + // SizedBox( + // height: 4.h, + // ), + // Text( + // S.of(context).guanzhu, + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ), + // ), + // Expanded( + // child: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: () {}, + // child: Container( + // color: Colors.transparent, + // padding: EdgeInsets.all(16), + // child: Column( + // children: [ + // Text( + // memberInfor != null + // ? memberInfor.fans.toString() + // : "0", + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 16.sp, + // fontWeight: + // MyFontWeight.semi_bold, + // ), + // ), + // SizedBox( + // height: 4.h, + // ), + // Text( + // S.of(context).fensi, + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ), + // ), + // Expanded( + // child: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: () {}, + // child: Container( + // color: Colors.transparent, + // padding: EdgeInsets.all(16), + // child: Column( + // children: [ + // Text( + // (memberInfor?.trendTotal ?? 0) + // .toString(), + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 16.sp, + // fontWeight: + // MyFontWeight.semi_bold, + // ), + // ), + // SizedBox( + // height: 4.h, + // ), + // Text( + // S.of(context).dongtai, + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ), + // ), + // Expanded( + // child: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: () {}, + // child: Container( + // color: Colors.transparent, + // padding: EdgeInsets.all(16), + // child: Column( + // children: [ + // Text( + // (memberInfor?.gainLikeTotal ?? 0) + // .toString(), + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 16.sp, + // fontWeight: + // MyFontWeight.semi_bold, + // ), + // ), + // SizedBox( + // height: 4.h, + // ), + // Text( + // S.of(context).huozan, + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ), + // ), + // ], + // ), + // ), + // Container( + // margin: EdgeInsets.all(16), + // height: 1.h, + // color: Color(0xFFF7F7F7), + // ), + // Padding( + // padding: EdgeInsets.only(left: 16.w), + // child: Text( + // memberId != "0" ? "TA的动态" : "我的动态", + // style: TextStyle( + // color: Color(0xFF353535), + // fontSize: 15.sp, + // fontWeight: MyFontWeight.semi_bold, + // ), + // )), + // dynamicList(), + // ], + // ), + // ), ], )), )); } + ///个人信息 Widget homeInfo() { return Positioned( top: 0, @@ -711,8 +801,8 @@ class _PersonalPage extends State with WidgetsBindingObserver { child: MImage( memberInfor?.headimg ?? "", isCircle: true, - width: 66, - height: 66, + width: 66.h, + height: 66.h, fit: BoxFit.cover, errorSrc: "assets/image/default_user.webp", fadeSrc: "assets/image/default_user.webp", @@ -831,6 +921,409 @@ class _PersonalPage extends State with WidgetsBindingObserver { )); } + ///个人信息 + Widget trendsInfo() { + return Container( + color: Colors.white, + margin: EdgeInsets.only(top: 17.h,left: 16.w,right: 16.w), + child:Column( + children: [ + ///个人信息 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoViewGalleryScreen( + images: [ + (memberInfor?.headimg ?? "").isEmpty + ? "https://i.postimg.cc/Pq6vjfnw/default-1.webp" + : memberInfor?.headimg + ], //传入图片list + index: 0, //传入当前点击的图片的index + ), + )); + }, + child: MImage( + memberInfor?.headimg ?? "", + isCircle: true, + width: 69.h, + height: 69.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_user.webp", + fadeSrc: "assets/image/default_user.webp", + ), + ), + Expanded(child:Container(padding:EdgeInsets.only(left: 10.w) + ,child: Column( + children: [ + Row( + children: [ + Text( + ((memberInfor?.nickname ?? "") == "") + ? "回乡" + : memberInfor?.nickname, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16.sp, + fontWeight: MyFontWeight.medium, + color: Color(0xFF353535), + ), + ), + SizedBox(width: 4.w), + Image.asset( + "assets/image/vip_yk.webp", + width: 20, + height: 20, + ), + Spacer(), + if (memberId == "0") + GestureDetector( + onTap: () { + setState(() { + _toUserInfo(); + }); + }, + child: Container( + height: 23.h, + padding: EdgeInsets.only( + left: 6.w, right: 6.w, bottom: 2.h, top: 2.h), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all( + width: 1.w, + color: Color(0xFF353535), + style: BorderStyle.solid, + ), + ), + child: Text( + S.of(context).bianjigerenziliao, + style: TextStyle( + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + color: Color(0xFF353535), + ), + ), + ), + ), + ], + ), + SizedBox(height: 13.h), + Container( + child: GestureDetector( + onTap: () { + setState(() { + isShrink = !isShrink; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 1, + child: Text( + memberId == "0" + ? ((memberInfor?.signature == "") + ? "还未编辑个性签名~" + : memberInfor?.signature ?? "") + : "个性签名: ${(memberInfor?.signature == "") ? "还未编辑个性签名~" : memberInfor?.signature ?? ""}", + overflow: isShrink + ? TextOverflow.visible + : TextOverflow.ellipsis, + maxLines: isShrink ? 10 : 2, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFF868686), + fontWeight: MyFontWeight.regular, + height: 1.5.h), + )), + Icon( + (isShrink != null && !isShrink) + ? Icons.keyboard_arrow_down + : Icons.keyboard_arrow_up, + color: Colors.black, + size: 18, + ), + ], + ), + )), + ], + ))), + ], + ), + ///关注,粉丝,动态,获赞,好友数量 + Container( + margin: EdgeInsets.only(top:24.h,left:10.w,right:10.w,bottom:19.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Text( + memberInfor != null + ? memberInfor.follow.toString() + : "0", + style: TextStyle( + color: Color(0xFF353535), + fontSize: 16.sp, + fontWeight: + MyFontWeight.semi_bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + S.of(context).guanzhu, + style: TextStyle( + color: Color(0xFF353535), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + )), + ), + ), + SizedBox(width:56.w,), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Text( + memberInfor != null + ? memberInfor.fans.toString() + : "0", + style: TextStyle( + color: Color(0xFF353535), + fontSize: 16.sp, + fontWeight: + MyFontWeight.semi_bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + S.of(context).fensi, + style: TextStyle( + color: Color(0xFF353535), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + )), + ), + ), + SizedBox(width:47.w,), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Text( + (memberInfor?.trendTotal ?? 0) + .toString(), + style: TextStyle( + color: Color(0xFF353535), + fontSize: 16.sp, + fontWeight: + MyFontWeight.semi_bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + S.of(context).dongtai, + style: TextStyle( + color: Color(0xFF353535), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + )), + ), + ), + SizedBox(width:44.w,), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Text( + (memberInfor?.gainLikeTotal ?? 0) + .toString(), + style: TextStyle( + color: Color(0xFF353535), + fontSize: 16.sp, + fontWeight: + MyFontWeight.semi_bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + S.of(context).huozan, + style: TextStyle( + color: Color(0xFF353535), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + )), + ), + ), + SizedBox(width:50.w,), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + children: [ + Text( + (memberInfor?.gainLikeTotal ?? 0) + .toString(), + style: TextStyle( + color: Color(0xFF353535), + fontSize: 16.sp, + fontWeight: + MyFontWeight.semi_bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + S.of(context).haoyou, + style: TextStyle( + color: Color(0xFF353535), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + )), + ), + ), + ], + ), + ), + Container( + height: 1.h, + color:Color(0xFFF7F7F7), + margin:EdgeInsets.only(bottom:15.h), + ), + ///关注,聊天按钮 + if (memberId != "0") + Row( + children: [ + Expanded(child:GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + + }, + child: Container( + // padding: EdgeInsets.symmetric(vertical:12.h), + height:41.h, + margin: EdgeInsets.only(right: 8.w,left: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.5), + color: (articles.isNotEmpty && articles.first.followed ?? false) + ? Color(0xFFE6E6E6) + : Color(0xFF32A060) + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + (articles.isNotEmpty && articles.first.followed ?? false) + ? Icons.check + : Icons.add, + color: (articles.isNotEmpty && articles.first.followed ?? false) + ? Color(0xFF808080) + : Colors.white, + size: 18.sp, + ), + SizedBox(width:5,), + Text( + (articles.isNotEmpty && articles.first.followed ?? false) + ? S.of(context).yiguanzhu + : S.of(context).guanzhu, + style: TextStyle( + fontSize: 12.sp, + color: (articles.isNotEmpty && articles.first.followed ?? false) + ? Color(0xFF808080) + : Colors.white, + fontWeight: MyFontWeight.regular, + )), + ], + ), + ), + )), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context) + .pushNamed('/router/chat_details_page', arguments: { + "toId": memberInfor.id, + },); + + }, + child: Container( + width:110.w, + height:41.h, + margin: EdgeInsets.only(right:1.w), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.5), + color: Color(0xFFFF9640) + ), + child: Text( + "聊一聊", + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + fontWeight: MyFontWeight.regular, + )), + ), + ) + ], + ), + ], + ), + ); + } + + ///动态 Widget dynamicList() { return Container( child: (articles == null || articles.length == 0) diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index d00fa53c..7cbe6f65 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -445,6 +445,11 @@ abstract class ApiService { Future>> followList( @Body() Map map); + /// 好友列表 + @POST("/member/mutualFollow/list") + Future>> mutualFollowList( + @Body() Map map); + ///关注/取关会员 @PUT("/member/follow/{followId}") Future follow(@Path("followId") String followId); diff --git a/lib/retrofit/retrofit_api.g.dart b/lib/retrofit/retrofit_api.g.dart index a2d78c6a..752b8278 100644 --- a/lib/retrofit/retrofit_api.g.dart +++ b/lib/retrofit/retrofit_api.g.dart @@ -1412,6 +1412,32 @@ class _ApiService implements ApiService { return value; } + @override + Future>> mutualFollowList(map) async { + ArgumentError.checkNotNull(map, 'map'); + const _extra = {}; + final queryParameters = {}; + final _data = {}; + _data.addAll(map ?? {}); + final _result = await _dio.request>( + '/member/mutualFollow/list', + queryParameters: queryParameters, + options: RequestOptions( + method: 'POST', + headers: {}, + extra: _extra, + baseUrl: baseUrl), + data: _data); + final value = BaseData>.fromJson( + _result.data, + (json) => PageInfo.fromJson( + json, + (json) => ListData.fromJson(json), + ), + ); + return value; + } + @override Future> follow(followId) async { ArgumentError.checkNotNull(followId, 'followId'); From 90798e33d51c74da822be94145975c92c47f912c Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Wed, 18 Sep 2024 16:20:25 +0800 Subject: [PATCH 05/81] =?UTF-8?q?out=E5=8E=8B=E7=BC=A9=E5=8C=85=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=EF=BC=9B=20=E6=96=B0=E5=A2=9E=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3=EF=BC=9B=20=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A1=B5=E9=9D=A2=E6=9B=B4=E6=94=B9=EF=BC=9B=20?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E5=88=97=E8=A1=A8=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_friend_group.dart | 10 +- lib/im/im_view/friend_groip_list.dart | 147 +++++++++++++++++++++++--- 2 files changed, 140 insertions(+), 17 deletions(-) diff --git a/lib/im/chat_friend_group.dart b/lib/im/chat_friend_group.dart index f0b1af9e..3bd669b4 100644 --- a/lib/im/chat_friend_group.dart +++ b/lib/im/chat_friend_group.dart @@ -37,7 +37,7 @@ class _ChatFriendGroup extends State } loadFinish() { - _allKey = [GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey()]; + _allKey = [GlobalKey(), GlobalKey(), GlobalKey()]; setState(() {}); } @@ -46,7 +46,7 @@ class _ChatFriendGroup extends State return Scaffold( backgroundColor: Color(0xFFFFFFFF), appBar: MyAppBar( - title: "好友(2)", + title: "${S.of(context).haoyou}(2)", titleColor: Color(0xFF0D0D0D), titleSize: 17.sp, leading: true, @@ -120,9 +120,9 @@ class _ChatFriendGroup extends State child: TabBarView( controller: tabController, children: [ - FriendGroupList(_allKey[0]), - FriendGroupList(_allKey[1]), - FriendGroupList(_allKey[2]), + FriendGroupList(_allKey[0],"","好友"), + FriendGroupList(_allKey[1],"false", "关注"), + FriendGroupList(_allKey[2],"true","粉丝"), ], ), ) diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index b7d87fc5..5acb9abb 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -1,12 +1,24 @@ +import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; - +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../retrofit/data/base_data.dart'; +import '../../retrofit/data/follow_list.dart'; +import '../../retrofit/data/page.dart'; import '../../retrofit/retrofit_api.dart'; +import 'package:huixiang/im/database/message.dart'; + +import '../../view_widget/custom_image.dart'; +import '../../view_widget/no_data_view.dart'; class FriendGroupList extends StatefulWidget { + final String isMyFans; + final String title; + FriendGroupList( - Key key, + Key key,this.isMyFans,this.title ) : super(key: key); @override @@ -18,11 +30,22 @@ class FriendGroupList extends StatefulWidget { class _FriendGroupList extends State { ApiService apiService; final TextEditingController editingController = TextEditingController(); + RefreshController _refreshController; FocusNode _focusNode = FocusNode(); + List userIds = []; + List messages = []; + int pageNum = 1; + List list = []; @override void initState() { super.initState(); + // loadMessageList(); + if( widget.isMyFans == ""){ + queryMutualFollowList(); + } else{ + queryFollowList(); + } } ///离开页面记着销毁和清除 @@ -32,6 +55,86 @@ class _FriendGroupList extends State { super.dispose(); } + // loadMessageList() async { + // SharedPreferences shared = await SharedPreferences.getInstance(); + // String userId = shared.getString("userId"); + // messages = await hxDatabase.queryList(userId); + // messages.forEach((element) { + // debugPrint("messages: ${element.toJson()}"); + // }); + // userIds = messages + // .map((e) => e.toId != userId ? e.toId : e.fromId) + // .toSet().where((element) => element != userId) + // .toList(); + // if (mounted) { + // setState(() {}); + // } + // + // } + + ///关注、粉丝列表 + queryFollowList() async { + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); + if (apiService == null) + apiService = ApiService( + Dio(), + context: context, + token: sharedPreferences.getString("token"), + showLoading: false, + ); + BaseData> baseData = await apiService.followList({ + "isMyFans": widget.isMyFans, + "pageNum": pageNum, + "pageSize": 100, + }).catchError((error) { + _refreshController.refreshFailed(); + _refreshController.loadFailed(); + }); + if (baseData != null && baseData.isSuccess) { + if (pageNum == 1) { + list.clear(); + } + list.addAll(baseData.data.list); + setState(() {}); + } + } + + + + ///好友列表 + queryMutualFollowList() async { + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); + if (apiService == null) + apiService = ApiService( + Dio(), + context: context, + token: sharedPreferences.getString("token"), + showLoading: false, + ); + BaseData> baseData = await apiService.mutualFollowList({ + "isMyFans": widget.isMyFans, + "pageNum": 1, + "pageSize": 100, + }).catchError((error) { + _refreshController.refreshFailed(); + _refreshController.loadFailed(); + }); + + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + if (baseData != null && baseData.isSuccess) { + if (pageNum == 1) { + list.clear(); + } + list.addAll(baseData.data.list); + print("list: ${list.length}"); + if (int.tryParse(baseData.data.total) < (pageNum * 10)) { + _refreshController.loadNoData(); + } + setState(() {}); + } + } + @override Widget build(BuildContext context) { return Container( @@ -39,13 +142,30 @@ class _FriendGroupList extends State { child: Column( children: [ friendGroupSearch(), - Expanded( + (list == null || list.length == 0) + ? NoDataView( + src: "assets/image/guan_zhu.webp", + isShowBtn: false, + text: "目前暂无${widget?.title ?? ""},听说多发动态可以涨粉哦~", + fontSize: 16.sp, + margin: EdgeInsets.only(top: 120.h,left: 60.w,right: 60.w), + ):Expanded( child: ListView.builder( - itemCount: 10, + itemCount: list.length ?? 0, physics: BouncingScrollPhysics(), shrinkWrap: true, itemBuilder: (context, position) { - return friendGroupItem(); + return GestureDetector( + onTap:(){ + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toId":list[position].mid, + }, + ); + }, + child:friendGroupItem(list[position]), + ); }, )), ], @@ -53,7 +173,7 @@ class _FriendGroupList extends State { ); } - Widget friendGroupItem() { + Widget friendGroupItem(ListData list) { return Container( margin: EdgeInsets.only(left:16.w,right:16.w,bottom: 24.h), child: Row(children: [ @@ -61,17 +181,20 @@ class _FriendGroupList extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(26.5.r), ), - child: Image.asset( - "assets/image/bs_mine_heading.webp", - width: 54.h, - height: 54.h, - fit: BoxFit.fill, + child: MImage( + list?.avatar ?? "", + isCircle: true, + width: 66, + height: 66, + fit: BoxFit.cover, + errorSrc: "assets/image/default_user.webp", + fadeSrc: "assets/image/default_user.webp", ), ), Padding( padding: EdgeInsets.only(left: 4.w), child: Text( - "哈喽喽哈", + list?.nickname ?? "", style: TextStyle( fontSize: 16.sp, color: Color(0xFF060606), From 3589c158d7d6636579b6e403ff16eeb0d23597be Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Wed, 18 Sep 2024 18:12:45 +0800 Subject: [PATCH 06/81] =?UTF-8?q?=E7=A4=BE=E7=BE=A4=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=8A=A8=E6=8C=89=E9=92=AE=E5=A4=84=E7=90=86=EF=BC=9B=20?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E9=A1=B5=E9=9D=A2=E6=95=B0=E9=87=8F=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A4=84=E7=90=86=EF=BC=9B=20=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=A9=BA=E6=95=B0=E6=8D=AE=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=9B=20=E6=B6=88=E6=81=AF=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E6=9D=A1=E7=94=B0=E5=AF=B9=E8=AF=9D=E6=A1=86=E9=80=80?= =?UTF-8?q?=E5=87=BA=E5=88=B7=E6=96=B0=E5=88=97=E8=A1=A8=EF=BC=9B=20?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E5=88=97=E8=A1=A8=E6=A0=87=E9=A2=98=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/image/2x/chat_more.webp | Bin 0 -> 868 bytes assets/image/2x/fa_bu.webp | Bin 868 -> 1560 bytes assets/image/3x/chat_more.webp | Bin 0 -> 1213 bytes assets/image/3x/fa_bu.webp | Bin 1213 -> 2360 bytes assets/image/chat_more.webp | Bin 0 -> 479 bytes assets/image/fa_bu.webp | Bin 479 -> 974 bytes lib/im/chat_details_page.dart | 2 +- lib/im/chat_friend_group.dart | 35 +++++++++++++++++++++++++- lib/im/im_view/friend_groip_list.dart | 19 +++++++------- lib/im/im_view/im_page.dart | 4 ++- lib/retrofit/data/social_info.dart | 12 ++++++++- lib/retrofit/retrofit_api.dart | 2 +- 12 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 assets/image/2x/chat_more.webp create mode 100644 assets/image/3x/chat_more.webp create mode 100644 assets/image/chat_more.webp diff --git a/assets/image/2x/chat_more.webp b/assets/image/2x/chat_more.webp new file mode 100644 index 0000000000000000000000000000000000000000..bb796014436893bf756afcc5180f1d2fb8519975 GIT binary patch literal 868 zcmV-q1DpJbP)ob>$o z#Y4vu0wf$7gNO(K#TdPQEmsBr=Uhl1nEBkYe^=Xp5I~Ia6TgC&f}L}r1-aFPh=>YE zaH_T_kaHJcz2=GuEf8TN6i>C0Zl$fUV9s6EkSZcXMCXhEyhNa)?P6kg6Y}0)VYK3$ zJJpcLAxEr))FPpriLEt<5=;?Q=I~6ubnNCleGf? zy}R6cjkXn%*`+Z=`-!q1;s{tC2>`lRHMbmyh^&|6Jp`$NQNoDOOC!eUJL3>|M|oDk z%KTp2pdN!$MQ)4Fm#_7*@2W#Ct?a$O#u#a_pg^p_&z14s`xO&A!dTA^Yi@uLvKytAW+xpn z8qYyf`?z=gGOf!buH7t*wM8g-%8~|?Gg`W56}=lW0ny)QQ;7a+Z^Rg{k8tUd-nmIySEuze9jrbOQP9swD=5hGwm2G7 zO>FK&*Jej!i~w0;NdSo3Upl;`+MrA^UXvEBIbL=)Ld}6wwR_6e~uAtBC`)Z%2Iq7Oh^QUtV|fq^jv$QDV5 zuq#09BnZ0)#4ZX6at5kd0Ay>VBe9c^*ySY!B|tSNKIgA?khp~qQ(3^k zz~92aup2H^_~3|!GLha&VM;sS?X6@YqVfd0$} z!gPilhExUx22X}OhGYgkAj=4tNDLXwfGNi35;FsXey)SGK=>ntNh+?ZUL{|vkCMN| zcH3=|$XA)gU$0fqKm1%?viC8NuerK<=EJRg>t1Iw-xWQ)ZK4Rfv3L1-9>x#%(hp7j zy#4xn&*)osYu?NcV4HC3>(_V>OSj025`Q0m-{^JX!66-2A#9+KL_%!>dnnR_jR)6<#~72JSVLw&#c}yx5+fh zQpr>PaYm|0ZtwbuDlf~Ea&0b2MXP(}C)etndZjZtrDfwC&Wkr|jl904|6O~J&GVR~ zv~BuaONHd83CAvH=}k)6vhhaA<#&8Do8FYnE!p6{OtG3-e6B`}a!n^E`&=8f%M%}; z{8i<(OlA4x()DR?mYnIVS}kfb@d9(RRlaUzpwau6VdZL~|Mm<2tY7~oPOEC2%AUy3 z`990EjIS>8J;=qW7oaJ0l`r>T<9e?PYQfWPekwVl>CACP$!oIM@jL1|?pmi`ow%}8 zg}KCE(OKx+qbD6R8@ac?EQf4#lwlj)g>xz_pL{Q2&8KMXmRb~@Wl@xe2niFV7Ick<`F zO8DF?Wa-asD%me)aMZx%$41QrM^5eo(}cENu<#TWJ-PANghCEZhr^k=KIgL&>pZ!= zS4TyZu2{Y2S%D_pI%YOI^Ka7Ai-(#)k%*Vcpyt_ZbGnmQ+H>a4W~7T;*?;Ie6n{W0h2_0tSx ziv9)JphlVjz^Pbu}Wwur0FJR{S;3 zic;GnR#r)OwYK`riI^U^rg_yhS+UnO-KAfTs)-$0W_U!+_sFu0BWf*2md!t+wtLI6 oYe&>RZCS>9R84d1GMBAt30s#{Z&jPVblIl=-onq5E`MPF09B43OaK4? literal 868 zcmV-q1DpJbP)ob>$o z#Y4vu0wf$7gNO(K#TdPQEmsBr=Uhl1nEBkYe^=Xp5I~Ia6TgC&f}L}r1-aFPh=>YE zaH_T_kaHJcz2=GuEf8TN6i>C0Zl$fUV9s6EkSZcXMCXhEyhNa)?P6kg6Y}0)VYK3$ zJJpcLAxEr))FPpriLEt<5=;?Q=I~6ubnNCleGf? zy}R6cjkXn%*`+Z=`-!q1;s{tC2>`lRHMbmyh^&|6Jp`$NQNoDOOC!eUJL3>|M|oDk z%KTp2pdN!$MQ)4Fm#_7*@2W#Ct?a$O#u#a_pg^p_&z14s`xO&A!dTA^Yi@uLvKytAW+xpn z8qYyf`?z=gGOf!buH7t*wM8g-%8~|?Gg`W56}=lW0ny)QQ;7a+Z^Rg{k8tUd-nmIySEuze9jrbOQP9swD=5hGwm2G7 zO>FK&*Jej!i~w0;NdSo3Upl;`+MrA^UXvEBIbL=)Ld}6wwR_6eB${fY|$nh=|iPy~4LJn-D_0 z0RRxukR!e5`i{`4_GDKEZV2za#5FDgHOPq3=cenzrRqcJyfXCWMCZC9!cLd4dx55jR30+-Mp zAmYHi#L^HEakZ>%b-{LA4QtacaGIvqO5!c49ca^ZL6c#^hEHw%C92zXR5RO^-*kEWD7lCa|(=_$c?lCmt_+2vb z$mL%_xFPKjnJ1M902ZSo$oyzCxkOt_;UTGI<YqEDQGP^2T$HbL!2${s3 zGd`sL&o0xBI4#*+bp=9*QF3jRS{XVM&Eg{9jHy`VaCJC~!@V;j~Pa49RVuZFSf`;M!OWouP#@2O`;^kv|l(N)Gen>zZ_|Ar=rfbhFMbm97{3gm;I5N?$)4G&Ia)jnk<2(W}9L_ZC=c>N64{MBG%?@E|Yf) zyjMDArO`phP(BV3c+Vtu6=p6GW5t@uTJslKcb2$}l>qnv$!3xO&H&57^h*R|C>hRn z0H`_$S~LLQE-M{p3+$o%lp`6%0F}H^jgZ{~Kvqkv5?ZqA+S++I9?f%1fMn9w>;TI( bdnx|_iJI}WS+UF&00000NkvXXu0mjfL^C~7 literal 0 HcmV?d00001 diff --git a/assets/image/3x/fa_bu.webp b/assets/image/3x/fa_bu.webp index 35fb8206db9c263d48ec884d7594d1c7447af5a1..b24c87ae26b32543d69705367ab02e4a23e57cd3 100644 GIT binary patch literal 2360 zcmb`Fc{J4P8^GUiEprovEW^E)xo9S9jGZiv#!?!4p%`128OtzftiLSb8e>TI5K)#& zYDm@+rbLu=M5L~^(TH>j_dEBVPWRl?@BIGxJ>U0zp3nDL&ilLz4vWEP3IX7Hz{0`B zLEjAq0MLYHs$)!lA_h*yandc);Hr#buwrIfKjLPB=>_rwn4%fPb*h zKloHcXatng$IZuwhI4reVq-#ZBzG;Ya68`aM|5_CtQ$0C0SVxMHL!sA|0>_kGvp!w z`YQnNW_|Pd6adha41m<+Hy@%50FfjB7=xkS;og5W!v|>|e}4d$ssRvo2SDsG0I*|U z8~s-M?JoYyUge=0S!kaj&=&xLfgg|uSU?1Lpbn8X^hmS-8hSByOZ)(6RDwOCi}Ui? zzODtgSEp6r`+0_UCQ*y;l?eCJ5e7CRd#khfJ0){I%z%wtRwf*atL!hwV|oX+G^DRb z>pW^Ez?lZff;KNWC^KoAJ#%kmgiR@2S(gcz@>(^BBNMtfM&hO5%tz(p!!o7sF#R?o0dl9q&- zAJsWB+leMBYAFjtScs%`!q^EoMFbz;PALUGQHPs#k8+g-B`%>|kSOzIzq>Z*YB7l# z`&`&Bp{lRWUXn^*;RuRbDqF}~31+DsQLxhTl|QD9cEp=V)b34DOxZ53X0BkBiq?gR z+ay?A-hX~Si$Qoo9-$E++s(-Ni8a%zVMB~` zCo>0u&E^i;VqwujNRX=iGQ~c8qq9XVT7(ysyQ6*~KFlcK7_QYV$K~nmGz4~dsLHvE-u}Z;U;2kZV5PR)lCHkzIQQqOIQZ z58JXF;0xP+c{>)o67pg#TmG8ND~ond6_w@|eERZu73@q+;#s8{qnH}|lzoY5ek})w zgQ`(ks@1wRZasOBNp04BlmD2O+JId99ABu@Sye$)T26}9Twlv)r!nd!PZ-%mN7wFY zL4Wig7G!zI3a>tvj4E;`{dh+M#<(~jv|5%|fBCoKfbgE#K)<+QrI&A8u#R;`7FVxL zX&tYozeUk9isTT~hOX4{XE2GWIyAXSao6S9=18rV^ks=Ges}xNrg_*L&4Qhb&G8E1 zqmy4>>gVwI2`z=d7j|4`%i9&5{ z?xIWle&4`kRJx_(R8G2HxTCLd+?PmgagWvEly|noAr9t(tzdKvX3*C5a{Gq6TyHBZ zIZtx=1UiXQ7G9m+6vl~EZEdd+v;!>;Qz2YR~tmU==&2p$uh??4BUrM~%l@t}P7B8Ms zH|py5d9Fu!#SRJkwPVq67PUK3g-lFKZH^4lS2%r!)^;J46g`L7G?e;GjfTJ(bT2)gE@uE2g}cS+D!MwDUH zkK)n=9A?jyyRyH+KJC&XY5~WtUn>yLz`#FxP3D_F?e?fc``^;lGP_PKI8W$zC-{5{ tfq%SQluw_&JGF@($UZR_Ber{B%6m>$Re3-eKhtWwmAFrKLf=&!{0U;4pF98n literal 1213 zcmV;u1Va0XP)B${fY|$nh=|iPy~4LJn-D_0 z0RRxukR!e5`i{`4_GDKEZV2za#5FDgHOPq3=cenzrRqcJyfXCWMCZC9!cLd4dx55jR30+-Mp zAmYHi#L^HEakZ>%b-{LA4QtacaGIvqO5!c49ca^ZL6c#^hEHw%C92zXR5RO^-*kEWD7lCa|(=_$c?lCmt_+2vb z$mL%_xFPKjnJ1M902ZSo$oyzCxkOt_;UTGI<YqEDQGP^2T$HbL!2${s3 zGd`sL&o0xBI4#*+bp=9*QF3jRS{XVM&Eg{9jHy`VaCJC~!@V;j~Pa49RVuZFSf`;M!OWouP#@2O`;^kv|l(N)Gen>zZ_|Ar=rfbhFMbm97{3gm;I5N?$)4G&Ia)jnk<2(W}9L_ZC=c>N64{MBG%?@E|Yf) zyjMDArO`phP(BV3c+Vtu6=p6GW5t@uTJslKcb2$}l>qnv$!3xO&H&57^h*R|C>hRn z0H`_$S~LLQE-M{p3+$o%lp`6%0F}H^jgZ{~Kvqkv5?ZqA+S++I9?f%1fMn9w>;TI( bdnx|_iJI}WS+UF&00000NkvXXu0mjfL^C~7 diff --git a/assets/image/chat_more.webp b/assets/image/chat_more.webp new file mode 100644 index 0000000000000000000000000000000000000000..66da3c3f79b6582616544ec2e39faa8ef539d889 GIT binary patch literal 479 zcmV<50U-W~P)t?Jl-)93r~ z3tV&(LPCTFn3(}URYzMYUjYCjN+L3~>`+ah7-zREM3jnf^m{~f1Ar~jRUu}cECM3B z76Vdo%JXkM5hBJo#~3xnI5){Ff-y#A=DLt0@?MdpjisX}Pf*pdg#PiYP|jIG2noOx zLZF7b~=OK>`5XQw0J3{EAU$uXZvB-vjtaA(&7@)RaIwZ=E07u>q`Cu zLn*vNV6~^Jqvc4B5;NOp0KAnS*M>Y^fbU!77fV^l(gqPFAJO4B1CXse6%WO)StNFS zTPU{0BH||H9A-xRJH3_AfnrzE@Irl{tP@`*)>Q(i??jh zCjiJf=MVw`cnu+>oOAB+xF+JBoaslXh{@%0VXVC)`$Sk(-E)`)3G&RCRnt ztQ`FWKvGI?ey?XZsSX=%9Gh$W+PF0p1HRX7j$37Kwck~RmZGjrnQ;OB%yX*(hA#sV V3x2J2ai#zO002ovPDHLkV1lP*#J$(bU=hK^z`$St#AaaR>FgXJ!35+oFiC(&7NAsaUP)1qyOTmh zWRwC(38p847`yH48j)}7`UQg4n^oi#03t&DggD!0R5Q{ zgy{@945m49N_7K$a0Okr*p}})b zva_#>=f#}6uH^!Pi44py4({&=4^;6ydv{`S&0_Y*87}9wx>Q#7o>w*UuV`dlBj8{& zZ{E-4wcB$RCp}ZD;$V+>^0@v6E7JzS)32oYkH=15GtriBujvt%QdR}`NlQFVrS?3& z5`XRKs#AOGUH(h%$X&AH*4<>iGqsMBR6Idw%f0yw4VUY5!5kIQ>p6@J4-&7xJuvGj z;{t=?h;Prt+#4R9DZKc?y5ZQFb$8BR$a#M24%Y(P&01f0d$#y4_1}8y+Rt8t8?VG) z#VIl2UJ0ZI!r}o|rZLyJL^OVcD@Q zAI`H>mrmt0UKPS@&c8bPe!jBDJDJS~m$oGz`(rtkZQ~nH)mW|YU`@Hzl0l+gtK_^w Y;}=Hhhej@YbtB;FA3f8mSN#qQ02h`Bc>n+a literal 479 zcmV<50U-W~P)t?Jl-)93r~ z3tV&(LPCTFn3(}URYzMYUjYCjN+L3~>`+ah7-zREM3jnf^m{~f1Ar~jRUu}cECM3B z76Vdo%JXkM5hBJo#~3xnI5){Ff-y#A=DLt0@?MdpjisX}Pf*pdg#PiYP|jIG2noOx zLZF7b~=OK>`5XQw0J3{EAU$uXZvB-vjtaA(&7@)RaIwZ=E07u>q`Cu zLn*vNV6~^Jqvc4B5;NOp0KAnS*M>Y^fbU!77fV^l(gqPFAJO4B1CXse6%WO)StNFS zTPU{0BH||H9A-xRJH3_AfnrzE@Irl{tP@`*)>Q(i??jh zCjiJf=MVw`cnu+>oOAB+xF+JBoaslXh{@%0VXVC)`$Sk(-E)`)3G&RCRnt ztQ`FWKvGI?ey?XZsSX=%9Gh$W+PF0p1HRX7j$37Kwck~RmZGjrnQ;OB%yX*(hA#sV V3x2J2ai#zO002ovPDHLkV1lP*# child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), child: Image.asset( - "assets/image/fa_bu.webp", + "assets/image/chat_more.webp", height: 26.h, width: 26.h, ), diff --git a/lib/im/chat_friend_group.dart b/lib/im/chat_friend_group.dart index 3bd669b4..c4d54953 100644 --- a/lib/im/chat_friend_group.dart +++ b/lib/im/chat_friend_group.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -8,9 +9,12 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../main.dart'; +import '../retrofit/data/base_data.dart'; +import '../retrofit/data/social_info.dart'; import '../utils/event_type.dart'; import '../view_widget/my_tab.dart'; import 'im_view/custom_underline_tabIndicator.dart'; @@ -28,12 +32,24 @@ class _ChatFriendGroup extends State ApiService apiService; TabController tabController; List _allKey = []; + SocialInfo infoNumber; @override void initState() { super.initState(); tabController = TabController(length: 3, vsync: this, initialIndex: 0); + tabController.addListener(() { + if(!tabController.indexIsChanging) + setState(() {}); + }); loadFinish(); + querySocialInfo(); + } + + @override + void dispose() { + super.dispose(); + tabController.dispose(); } loadFinish() { @@ -41,12 +57,29 @@ class _ChatFriendGroup extends State setState(() {}); } + ///个人社交信息(粉丝/关注数量/成就数量/好友数量) + querySocialInfo() async { + SharedPreferences value = await SharedPreferences.getInstance(); + apiService = ApiService(Dio(), + context: context, token: value.getString("token"), showLoading: true); + + BaseData baseData = + await apiService.socialInfo().catchError((onError) {}); + if (baseData != null && baseData.isSuccess) { + setState(() { + infoNumber = baseData.data; + }); + } + EasyLoading.dismiss(); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Color(0xFFFFFFFF), appBar: MyAppBar( - title: "${S.of(context).haoyou}(2)", + title: tabController.index == 0 ? "${S.of(context).haoyou} (${infoNumber?.mutualFollowCount ?? "0"})" : + (tabController.index == 1 ? "${S.of(context).guanzhu} (${infoNumber?.follow ?? "0"})" : "${S.of(context).fensi} (${infoNumber?.fans ?? "0"})"), titleColor: Color(0xFF0D0D0D), titleSize: 17.sp, leading: true, diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index 5acb9abb..cfdbfed8 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -95,6 +95,8 @@ class _FriendGroupList extends State { list.clear(); } list.addAll(baseData.data.list); + if(!mounted) + return; setState(() {}); } } @@ -119,18 +121,13 @@ class _FriendGroupList extends State { _refreshController.refreshFailed(); _refreshController.loadFailed(); }); - - _refreshController.refreshCompleted(); - _refreshController.loadComplete(); if (baseData != null && baseData.isSuccess) { if (pageNum == 1) { list.clear(); } list.addAll(baseData.data.list); - print("list: ${list.length}"); - if (int.tryParse(baseData.data.total) < (pageNum * 10)) { - _refreshController.loadNoData(); - } + if(!mounted) + return; setState(() {}); } } @@ -146,7 +143,8 @@ class _FriendGroupList extends State { ? NoDataView( src: "assets/image/guan_zhu.webp", isShowBtn: false, - text: "目前暂无${widget?.title ?? ""},听说多发动态可以涨粉哦~", + text: widget.title == "好友"?"目前暂无${widget?.title ?? ""}":("目前暂无${widget?.title ?? ""},${widget?.title == "粉丝" ? + "听说多发动态可以涨粉哦" :"可以在社群广场中关注自己喜欢的人哦"}~"), fontSize: 16.sp, margin: EdgeInsets.only(top: 120.h,left: 60.w,right: 60.w), ):Expanded( @@ -156,6 +154,7 @@ class _FriendGroupList extends State { shrinkWrap: true, itemBuilder: (context, position) { return GestureDetector( + behavior: HitTestBehavior.opaque, onTap:(){ Navigator.of(context).pushNamed( '/router/chat_details_page', @@ -184,8 +183,8 @@ class _FriendGroupList extends State { child: MImage( list?.avatar ?? "", isCircle: true, - width: 66, - height: 66, + width: 66.h, + height: 66.h, fit: BoxFit.cover, errorSrc: "assets/image/default_user.webp", fadeSrc: "assets/image/default_user.webp", diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 12ccec6e..235b4ada 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -229,7 +229,9 @@ class _IMPage extends State implements OnChatMessage { behavior: HitTestBehavior.opaque, onTap: () { Navigator.of(context) - .pushNamed('/router/chat_friend_group'); + .pushNamed('/router/chat_friend_group').then((value) { + _refresh(); + }); }, child: Container( padding: EdgeInsets.all(12), diff --git a/lib/retrofit/data/social_info.dart b/lib/retrofit/data/social_info.dart index cffbc3d4..184c1e54 100644 --- a/lib/retrofit/data/social_info.dart +++ b/lib/retrofit/data/social_info.dart @@ -5,24 +5,29 @@ class SocialInfo { SocialInfo({ dynamic fans, dynamic follow, - dynamic achievementNumber}){ + dynamic achievementNumber, + dynamic mutualFollowCount}){ _fans = fans; _follow = follow; _achievementNumber = achievementNumber; + _mutualFollowCount = mutualFollowCount; } SocialInfo.fromJson(dynamic json) { _fans = json['fans']; _follow = json['follow']; _achievementNumber = json['achievementNumber']; + _mutualFollowCount = json['mutualFollowCount']; } dynamic _fans; dynamic _follow; dynamic _achievementNumber; + dynamic _mutualFollowCount; dynamic get fans => _fans; dynamic get follow => _follow; dynamic get achievementNumber => _achievementNumber; + dynamic get mutualFollowCount => _mutualFollowCount; set fans(int value) { @@ -34,6 +39,7 @@ class SocialInfo { map['fans'] = _fans; map['follow'] = _follow; map['achievementNumber'] = _achievementNumber; + map['mutualFollowCount'] = _mutualFollowCount; return map; } @@ -44,4 +50,8 @@ class SocialInfo { set achievementNumber(int value) { _achievementNumber = value; } + + set mutualFollowCount(int value) { + _mutualFollowCount = value; + } } \ No newline at end of file diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index 7cbe6f65..970d44c0 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -458,7 +458,7 @@ abstract class ApiService { @DELETE("/information/trend/{id}") Future deleteTrend(@Path("id") String id); - ///个人社交信息(粉丝/关注数量/成就数量) + ///个人社交信息(粉丝/关注数量/成就数量/好友数量) @GET("/member/socialInfo") Future> socialInfo(); From 6fdc7e3b41e60b0519442d0ed327deaf206e6b4e Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 09:50:59 +0800 Subject: [PATCH 07/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 55 ++++++++++--------- lib/im/chat_details_page.dart | 14 +++++ lib/im/database/contact.dart | 33 ++++++++++++ lib/im/database/hx_database.dart | 93 ++++++++++++++++++++++++++------ lib/im/im_view/im_page.dart | 62 +++++++++++++++++++-- lib/im/out/auth.pb.dart | 1 + lib/im/out/message.pbenum.dart | 1 + lib/main.dart | 6 +-- lib/main_page.dart | 16 ++++-- 9 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 lib/im/database/contact.dart diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 2237e157..ee2bd1e9 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -1,9 +1,7 @@ - - - import 'dart:convert'; +import 'dart:core'; +import 'dart:core'; import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:huixiang/im/Proto.dart'; import 'package:huixiang/im/database/message.dart'; @@ -20,40 +18,48 @@ class SocketClient { connect() async { shared = await SharedPreferences.getInstance(); - await Socket.connect('192.168.10.200', 49168).then((value) { + await Socket.connect('192.168.10.129', 9090).then((value) { debugPrint("socket-connect"); _socket = value; _socket.listen((data) { print(data); print("socket-listen"); Proto proto = Proto.fromBytes(data); - MsgData data1 = MsgData.fromBuffer(proto.body); - print('收到来自:${data1.from},消息内容: ${utf8.decode(data1.data)} '); + MsgData dataResult = MsgData.fromBuffer(proto.body); + print('收到来自:${dataResult.from},消息内容: ${utf8.decode(dataResult.data)} '); - hxDatabase.insert(createMessage(userId, utf8.decode(data1.data), msgType: data1.type.value, userId: data1.from)); + Map messageMap = createMessage(userId, utf8.decode(dataResult.data), msgType: dataResult.type.value, userId: dataResult.from); + Message message = Message.fromJson(messageMap); + callbacks[userId]?.call(message); /// user self conversation callback + callbacks[dataResult.from]?.call(message); /// user conversation callback - callbacks.values.forEach((callback) { - callback.call(data1); - }); + hxDatabase.insert(messageMap); }, onError: (Object error, StackTrace stackTrace) { - debugPrint("socket-listen-error: $error, stackTrace: ${stackTrace}"); + debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); }); authRequest(shared.getString("token")); }).catchError((error) { debugPrint("socket-connect-error: $error"); - Future.delayed(const Duration(milliseconds: 3000), () { - connect(); - }); + reconnect(); }); } - Map callbacks = {}; + int reconnectTime = 1500; - addCallback(String userId, Function callback) { - callbacks.putIfAbsent(userId, callback); + reconnect() { + Future.delayed(Duration(milliseconds: reconnectTime *= 2), () { + dispose(); + connect(); + }); + } + + Map callbacks = {}; + + addCallback(String userId, callback) { + callbacks[userId] = callback; } removeCallback(String userId) { @@ -61,7 +67,9 @@ class SocketClient { } dispose() { - _socket.close(); + if (_socket != null) { + _socket.close(); + } } authRequest(String token) { @@ -78,7 +86,7 @@ class SocketClient { } Future sendMessage(String toId, String content) async { - Map message = createMessage(toId, content, userId: userId); + Map message = createMessage(toId, content, userId: userId); int id = await hxDatabase.insert(message).catchError((error) { debugPrint("insertMessage: $error"); }); @@ -92,7 +100,7 @@ class SocketClient { } message["id"] = id; Uint8List data = utf8.encode(content); - MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.COMMAND, data: data); + MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); _socket.add(proto2.toBytes()); debugPrint("sendMessage: ${message["id"]}"); @@ -101,13 +109,12 @@ class SocketClient { checkSocket() { if (_socket == null) { - connect(); + reconnect(); return false; } return true; } - get userId => shared.getString("userId"); - + String get userId => shared.getString("userId"); } \ No newline at end of file diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 049fdeb4..3d2881e6 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; @@ -66,6 +67,16 @@ class _ChatDetailsPage extends State toUserId = widget.arguments["toId"]; messages = await hxDatabase.queryUList(toUserId); + + socketClient.addCallback(toUserId, (Message message) { + messages.add(message); + + refreshState(); + }); + refreshState(); + } + + refreshState() { if (mounted) setState(() {}); } @@ -142,6 +153,9 @@ class _ChatDetailsPage extends State OnChatMsgInstance.instance.onChatMessage = _tempOnChatMessage; WidgetsBinding.instance.removeObserver(this); commentFocus.removeListener(_focusNodeListener); + + socketClient.removeCallback(toUserId); + } void _focusNodeListener() { diff --git a/lib/im/database/contact.dart b/lib/im/database/contact.dart new file mode 100644 index 00000000..559e7b2a --- /dev/null +++ b/lib/im/database/contact.dart @@ -0,0 +1,33 @@ +class Contact { + int id; + + String userId; + + String nickName; + + String imageUrl; + + int state; + + int isDelete; + + Contact(this.id, this.userId, this.nickName, this.imageUrl, this.state, + this.isDelete); + + factory Contact.fromJson(Map json) => Contact( + json["id"], + json["userId"], + json["nickName"], + json["imageUrl"], + json["state"], + json["isDelete"]); + + Map toJson() => { + "id": id, + "fromId": userId, + "toId": nickName, + "replyId": imageUrl, + "state": state, + "isDelete": isDelete == null ? 0 : isDelete + }; +} diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 460e63de..b74b739a 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; import 'package:sqflite/sqflite.dart'; @@ -9,27 +10,32 @@ class HxDatabase { Database db; - void open() async { + void open({String key}) async { // _migrations.add(Migration(1, 2, (Database database) async { // database.execute('ALTER TABLE `Message` ADD COLUMN `replyId` VARCHAR(20) DEFAULT NULL AFTER `toId`'); // })); + String databaseName = 'hx.db'; + if (key?.isNotEmpty ?? false) { + databaseName = 'hx_$key.db'; + } await openDatabase( - 'hx.db', - version: 2, - onCreate: (Database db, int version) async { - db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); - }, - onConfigure: (database) async { - await database.execute('PRAGMA foreign_keys = ON'); - }, - onUpgrade: (database, startVersion, endVersion) async { - await runMigrations(database, startVersion, endVersion, _migrations); - }, - onOpen: (Database db) { - this.db = db; - } + databaseName, + version: 2, + onCreate: (Database db, int version) async { + db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + db.execute('CREATE TABLE IF NOT EXISTS `Contact` (`id` INTEGER, `userId` VARCHAR(20), `nickName` VARCHAR(20), `imageUrl` VARCHAR(200), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + }, + onConfigure: (database) async { + await database.execute('PRAGMA foreign_keys = ON'); + }, + onUpgrade: (database, startVersion, endVersion) async { + await runMigrations(database, startVersion, endVersion, _migrations); + }, + onOpen: (Database db) { + this.db = db; + } ); } @@ -37,6 +43,22 @@ class HxDatabase { db.close(); } + Future lastMessage(String userId) async { + if (db == null) { + return Future.value(); + } + String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; + List messages = await db.rawQuery(sql, [userId, userId]).then((value) { + return value.map((e) { + debugPrint("Message: ${e}"); + return Message.fromJson(e); + }).toList(); + }, onError: (error) { + debugPrint("Messageerror: $error"); + }); + return (messages?.isNotEmpty ?? false) ? messages.first : null; + } + Future> queryList(userId) { if (db == null) { return Future.value([]); @@ -77,17 +99,54 @@ class HxDatabase { } update(Map message) { - + if (db == null) { + return Future.value(0); + } + debugPrint("Message_insert: $message"); + return db.update("Message", message, where: 'id = ?', whereArgs: [message['id']]); } Future insert(Map message) async { if (db == null) { return Future.value(0); } - debugPrint("Messageinsert: ${message}"); + debugPrint("Message_insert: $message"); return db.insert("Message", message); } + Future insertContact(Map contactMap) async { + if (db == null) { + return Future.value(0); + } + debugPrint("contact_insert: $contactMap"); + return db.insert("Contact", contactMap); + } + + Future> queryContact(List userIds) async { + if (db == null) { + return Future.value([]); + } + String sql = 'SELECT * FROM Contact WHERE userId IN (?) AND state = 0 AND isDelete = 0'; + return db.rawQuery(sql, userIds).then((value) { + return value.map((e) => Contact.fromJson(e)).toList(); + }, onError: (error) { + debugPrint("Contact_error: $error"); + }); + } + + Future queryContactById(String userId) async { + if (db == null) { + return Future.value(); + } + List contact = await db.query("Contact", distinct: true, where: "userId = ?", whereArgs: [userId]) + .then((value) { + return value.map((e) => Contact.fromJson(e)).toList(); + }, onError: (error) { + debugPrint("Contact_error: $error"); + }); + return (contact?.isNotEmpty ?? false) ? contact.first : null; + } + final List _migrations = []; addMigrations(List migrations) { diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 12ccec6e..147a35bb 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'dart:collection'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:huixiang/generated/l10n.dart'; +import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/data/msg_stats.dart'; @@ -53,6 +56,8 @@ class _IMPage extends State implements OnChatMessage { void dispose() { super.dispose(); OnChatMsgInstance.instance.onChatMessage = null; + + socketClient.removeCallback(socketClient.userId); } @override @@ -62,8 +67,7 @@ class _IMPage extends State implements OnChatMessage { loadMessageList(); SharedPreferences.getInstance().then((value) { - apiService = - ApiService(Dio(), token: value.getString("token"), context: context); + apiService = ApiService(Dio(), token: value.getString("token"), context: context); queryMsgStats(); }); } @@ -75,12 +79,33 @@ class _IMPage extends State implements OnChatMessage { } List userIds = []; - + Map contactMap = {}; + Map lastMessageMap = {}; Stream streamSubscription ; loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); + + debugPrint("messages: loadMessageList"); + socketClient.addCallback(userId, (Message message) { + if (userIds.contains(message.fromId)) { + userIds.remove(message.fromId); + } + userIds.insert(0, message.fromId); + + lastMessageMap[message.fromId] = message; + + debugPrint("messages_records : ${message.toJson()}"); + if (contactMap[message.fromId] == null) { + /// TODO: message.fromId request Api and setState + } + if (mounted) { + setState(() {}); + } + }); + + debugPrint("messages: queryList"); messages = await hxDatabase.queryList(userId); messages.forEach((element) { debugPrint("messages: ${element.toJson()}"); @@ -89,10 +114,36 @@ class _IMPage extends State implements OnChatMessage { .map((e) => e.toId != userId ? e.toId : e.fromId) .toSet().where((element) => element != userId) .toList(); + + lastMessageMap = groupBy(messages, (p0) => p0.toId != userId ? p0.toId : p0.fromId); + + List contacts = await hxDatabase.queryContact(userIds); + if (contacts?.isEmpty ?? false) { + /// TODO: userIds request Api + } else { + List queryUserIds = userIds.where((u) => contacts.where((c) => c.userId == u).isEmpty).toList(); + /// TODO: queryUserIds request Api + } + contactMap = groupBy(contacts, (p0) => p0.userId); + if (mounted) { setState(() {}); } + } + + void updateLastMessage(String userId) async { + Message message = await hxDatabase.lastMessage(userId); + if (message != null) { + lastMessageMap[userId] = message; + } + } + Map groupBy(Iterable values, S Function(T) key) { + var map = {}; + for (var element in values) { + map[key(element)] ??= element; + } + return map; } // queryMessage() async { @@ -326,7 +377,9 @@ class _IMPage extends State implements OnChatMessage { arguments: { "toId": userIds[position], }, - ); + ).then((value) { + updateLastMessage(userIds[position]); + }); }, child: chatItem(userIds[position]), ); @@ -427,4 +480,5 @@ class _IMPage extends State implements OnChatMessage { ), ); } + } diff --git a/lib/im/out/auth.pb.dart b/lib/im/out/auth.pb.dart index 8296a40c..abdf04cb 100644 --- a/lib/im/out/auth.pb.dart +++ b/lib/im/out/auth.pb.dart @@ -14,6 +14,7 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class AuthReq extends $pb.GeneratedMessage { + factory AuthReq({ $core.String? uid, $core.String? token, diff --git a/lib/im/out/message.pbenum.dart b/lib/im/out/message.pbenum.dart index df67c4a9..2ce14e1a 100644 --- a/lib/im/out/message.pbenum.dart +++ b/lib/im/out/message.pbenum.dart @@ -14,6 +14,7 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class MsgType extends $pb.ProtobufEnum { + static const MsgType COMMAND = MsgType._(0, _omitEnumNames ? '' : 'COMMAND'); static const MsgType TEXT = MsgType._(1, _omitEnumNames ? '' : 'TEXT'); static const MsgType IMAGE = MsgType._(2, _omitEnumNames ? '' : 'IMAGE'); diff --git a/lib/main.dart b/lib/main.dart index 6fa68f1c..3eb87255 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -208,8 +208,6 @@ void main() async { // initSdk(); bool isFirst = sharedPreferences.getBool("isFirst"); - initDatabase(); - runApp(MyApp(locale, isFirst)); // FlutterBugly.postCatchedException((){ // }); @@ -220,9 +218,9 @@ void main() async { HxDatabase hxDatabase; -initDatabase() async { +initDatabase(String userId) async { hxDatabase = HxDatabase(); - await hxDatabase.open(); + hxDatabase.open(key: userId); } final SocketClient socketClient = new SocketClient(); diff --git a/lib/main_page.dart b/lib/main_page.dart index ba5f1400..f2349dcf 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -57,6 +57,7 @@ class _MainPage extends State with WidgetsBindingObserver { final GlobalKey homePageKey = GlobalKey(); final GlobalKey minePageKey = GlobalKey(); final GlobalKey unionPageKey = GlobalKey(); + // final GlobalKey vipPageKey = GlobalKey(); final GlobalKey imPageKey = GlobalKey(); @@ -83,7 +84,7 @@ class _MainPage extends State with WidgetsBindingObserver { if (DateTime.now().millisecondsSinceEpoch - lastTime > 420000) //处于后台**分钟后刷新应用 // Navigator.of(context).popAndPushNamed('/router/start_page'); - setState((){}); + setState(() {}); break; case AppLifecycleState.paused: // 界面不可见,后台 lastTime = DateTime.now().millisecondsSinceEpoch; @@ -102,7 +103,11 @@ class _MainPage extends State with WidgetsBindingObserver { void initState() { super.initState(); - connectSocket(); + SharedPreferences.getInstance().then((value) { + String userId = value.getString("userId"); + initDatabase(userId); + connectSocket(); + }); pageController = PageController( initialPage: @@ -125,7 +130,8 @@ class _MainPage extends State with WidgetsBindingObserver { ..dismissOnTap = false; initSdk(); - UmengCommonSdk.initCommon('6491509087568a379b5a1345', '6491509087568a379b5a1345', 'Umeng'); + UmengCommonSdk.initCommon( + '6491509087568a379b5a1345', '6491509087568a379b5a1345', 'Umeng'); UmengCommonSdk.setPageCollectionModeManual(); initPlatformState(); @@ -431,8 +437,8 @@ class _MainPage extends State with WidgetsBindingObserver { xgFlutterPlugin.setAccount(mobile, AccountType.PHONE_NUMBER); // xgFlutterPlugin.unbindWithIdentifier(identify: mobile, bindType: XGBindType.account) xgFlutterPlugin.bindWithIdentifier( - identify: mobile, - bindType: XGBindType.account, + identify: mobile, + bindType: XGBindType.account, ); } } From afa45d7c95bcb4177cdac09e390f883768ac1eb0 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 12:18:33 +0800 Subject: [PATCH 08/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constant.dart | 46 ++++++++++++++++++++++++++++++++ lib/im/chat_details_page.dart | 3 +-- lib/im/database/hx_database.dart | 22 +++++++++++++++ lib/im/database/message.dart | 1 + lib/im/im_view/im_page.dart | 10 ++++--- 5 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 lib/constant.dart diff --git a/lib/constant.dart b/lib/constant.dart new file mode 100644 index 00000000..169bf5db --- /dev/null +++ b/lib/constant.dart @@ -0,0 +1,46 @@ + + + +Map> groupBy(Iterable values, S Function(T) key) { + var map = >{}; + for (var element in values) { + (map[key(element)] ??= []).add(element); + } + return map; +} + +Map groupCount(Map> values) { + var map = {}; + for (var element in values.keys) { + map["$element"] = values[element]?.length ?? 0; + } + return map; +} + +Map groupItem(Map> values) { + var map = {}; + for (var element in values.keys) { + if (values[element] == null) { + continue; + } + map["$element"] = values[element].first; + } + return map; +} + + +extension ListExtension on Iterable { + + Map> lGroupBy(S Function(T) key) { + return groupBy(this, key); + } + +} + +extension MapExtension on Map> { + + Map get mGroupCount => groupCount(this); + + Map get mGroupItem => groupItem(this); + +} diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index c1323852..30f41bff 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -64,13 +64,12 @@ class _ChatDetailsPage extends State loadMessageList() async { selfUserId = (await SharedPreferences.getInstance()).getString("userId"); - toUserId = widget.arguments["toId"]; + await hxDatabase.readMessage(selfUserId, toUserId); messages = await hxDatabase.queryUList(toUserId); socketClient.addCallback(toUserId, (Message message) { messages.add(message); - refreshState(); }); refreshState(); diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index b74b739a..14a8c86e 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; @@ -86,6 +87,19 @@ class HxDatabase { }); } + Future> messageUnreadCount(List userIds, String selfUserId) async { + if (db == null) { + return Future.value({}); + } + String sql = 'SELECT * FROM Message WHERE fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0'; + List messages = await db.rawQuery(sql, [userIds, selfUserId]).then((value) { + return value.map((e) => Message.fromJson(e)).toList(); + }, onError: (error) { + debugPrint("Message-error: $error"); + }); + return messages.lGroupBy((p)=> p.fromId).mGroupCount; + } + Future> queryListAll() { if (db == null) { return Future.value(); @@ -114,6 +128,14 @@ class HxDatabase { return db.insert("Message", message); } + /// update message read state + readMessage(String selfUserId, String userId) { + if (db == null) { + return Future.value([]); + } + db.update("Message", {"state": 1}, where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", whereArgs: [userId, selfUserId]); + } + Future insertContact(Map contactMap) async { if (db == null) { return Future.value(0); diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 741f8778..92ed21ed 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -16,6 +16,7 @@ class Message { String time; + /// 0 unread, 1 read, 2 int state; int isDelete; diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 1e0b5d22..1518ca38 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -4,6 +4,7 @@ import 'dart:collection'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; @@ -81,6 +82,7 @@ class _IMPage extends State implements OnChatMessage { List userIds = []; Map contactMap = {}; Map lastMessageMap = {}; + Map unreadCountMap = {}; Stream streamSubscription ; loadMessageList() async { @@ -115,7 +117,8 @@ class _IMPage extends State implements OnChatMessage { .toSet().where((element) => element != userId) .toList(); - lastMessageMap = groupBy(messages, (p0) => p0.toId != userId ? p0.toId : p0.fromId); + unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); + lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; List contacts = await hxDatabase.queryContact(userIds); if (contacts?.isEmpty ?? false) { @@ -192,8 +195,9 @@ class _IMPage extends State implements OnChatMessage { token: value.getString("token"), ); } - BaseData> baseData = - await apiService.stats().catchError((onError) {}); + BaseData> baseData = await apiService.stats().catchError((onError) { + debugPrint("stats.error: $onError"); + }); if (baseData != null && baseData.isSuccess) { setState(() { msgNumber.forEach((key, value) { From 148d75ad8dba8e502bb42820d36841d98ba880fa Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 12:26:19 +0800 Subject: [PATCH 09/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 1518ca38..e1a27538 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -127,7 +127,7 @@ class _IMPage extends State implements OnChatMessage { List queryUserIds = userIds.where((u) => contacts.where((c) => c.userId == u).isEmpty).toList(); /// TODO: queryUserIds request Api } - contactMap = groupBy(contacts, (p0) => p0.userId); + contactMap = contacts.lGroupBy((p0) => p0.userId).mGroupItem; if (mounted) { setState(() {}); @@ -138,15 +138,19 @@ class _IMPage extends State implements OnChatMessage { Message message = await hxDatabase.lastMessage(userId); if (message != null) { lastMessageMap[userId] = message; + refreshState(); } } - Map groupBy(Iterable values, S Function(T) key) { - var map = {}; - for (var element in values) { - map[key(element)] ??= element; - } - return map; + void updateUnreadCount() async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); + unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); + refreshState(); + } + + refreshState() { + if (mounted) setState(() {}); } // queryMessage() async { From fae3dc2f4aa1dd2028739c1d0619810211c545f2 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 12:27:32 +0800 Subject: [PATCH 10/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index e1a27538..8661e8e5 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -121,7 +121,7 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; List contacts = await hxDatabase.queryContact(userIds); - if (contacts?.isEmpty ?? false) { + if (contacts?.isEmpty ?? true) { /// TODO: userIds request Api } else { List queryUserIds = userIds.where((u) => contacts.where((c) => c.userId == u).isEmpty).toList(); From c8e4d9e2f3d73ba1fb2e15cbe1f678d85be9c76b Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 12:29:58 +0800 Subject: [PATCH 11/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 8661e8e5..54ca6ec9 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -102,9 +102,7 @@ class _IMPage extends State implements OnChatMessage { if (contactMap[message.fromId] == null) { /// TODO: message.fromId request Api and setState } - if (mounted) { - setState(() {}); - } + refreshState(); }); debugPrint("messages: queryList"); @@ -129,9 +127,7 @@ class _IMPage extends State implements OnChatMessage { } contactMap = contacts.lGroupBy((p0) => p0.userId).mGroupItem; - if (mounted) { - setState(() {}); - } + refreshState(); } void updateLastMessage(String userId) async { From dd27c91b0c19857078868627dccc050c03c07544 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 14:14:13 +0800 Subject: [PATCH 12/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/database/hx_database.dart | 11 +++++++---- lib/im/im_view/im_page.dart | 2 -- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 14a8c86e..5aec1815 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -51,11 +51,11 @@ class HxDatabase { String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; List messages = await db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) { - debugPrint("Message: ${e}"); + debugPrint("Message: $e"); return Message.fromJson(e); }).toList(); }, onError: (error) { - debugPrint("Messageerror: $error"); + debugPrint("Message_error: $error"); }); return (messages?.isNotEmpty ?? false) ? messages.first : null; } @@ -91,8 +91,11 @@ class HxDatabase { if (db == null) { return Future.value({}); } - String sql = 'SELECT * FROM Message WHERE fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0'; - List messages = await db.rawQuery(sql, [userIds, selfUserId]).then((value) { + List messages = await db.query( + "Message", + where: 'fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0', + whereArgs: []..addAll(userIds)..add(selfUserId) + ).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Message-error: $error"); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 54ca6ec9..d6d8b6f4 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -89,7 +89,6 @@ class _IMPage extends State implements OnChatMessage { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); - debugPrint("messages: loadMessageList"); socketClient.addCallback(userId, (Message message) { if (userIds.contains(message.fromId)) { userIds.remove(message.fromId); @@ -105,7 +104,6 @@ class _IMPage extends State implements OnChatMessage { refreshState(); }); - debugPrint("messages: queryList"); messages = await hxDatabase.queryList(userId); messages.forEach((element) { debugPrint("messages: ${element.toJson()}"); From d221f5e87c1ee032844c37484ecec5bb6f3b30fe Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Thu, 19 Sep 2024 15:19:03 +0800 Subject: [PATCH 13/81] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9B=B4=E6=94=B9=EF=BC=9B=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=A5=BD=E5=8F=8B=E5=BC=B9=E7=AA=97=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/add_friend.dart | 143 ++++++++++++++++++++++++---- lib/im/chat_details_page.dart | 77 +++++++-------- lib/im/database/contact.dart | 33 ------- lib/im/database/hx_database.dart | 97 +++++++++---------- lib/im/im_search.dart | 0 lib/im/im_view/im_page.dart | 98 ++++++++++++------- lib/im/im_view/time_formatter.dart | 20 ++++ lib/retrofit/data/im_user_list.dart | 61 ++++++++++++ lib/retrofit/retrofit_api.dart | 10 ++ lib/retrofit/retrofit_api.g.dart | 48 ++++++++++ 10 files changed, 404 insertions(+), 183 deletions(-) delete mode 100644 lib/im/database/contact.dart create mode 100644 lib/im/im_search.dart create mode 100644 lib/im/im_view/time_formatter.dart create mode 100644 lib/retrofit/data/im_user_list.dart diff --git a/lib/im/add_friend.dart b/lib/im/add_friend.dart index 0d66d634..71ca7307 100644 --- a/lib/im/add_friend.dart +++ b/lib/im/add_friend.dart @@ -96,7 +96,7 @@ class _AddFriend extends State { ); } - /// + ///添加好友列表 Widget addFriendItem() { return Container( margin: EdgeInsets.only(left:16.w,right:16.w,bottom: 24.h), @@ -122,32 +122,133 @@ class _AddFriend extends State { fontWeight: FontWeight.w500), ), ), - Container( - padding: EdgeInsets.symmetric(horizontal:18.w,vertical:6.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(26.5.r), - color: Color(0xFF32A060), - boxShadow: [ - BoxShadow( - color: Color(0xFF32A060).withOpacity(0.2), - spreadRadius: 0, - blurRadius: 4, - offset: Offset(0, 4), // changes position of shadow - ), - ], - ), - child: Text( - "添加", - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight:MyFontWeight.regular), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){ + showFriendVerificationDialog(); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal:18.w,vertical:6.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(26.5.r), + color: Color(0xFF32A060), + boxShadow: [ + BoxShadow( + color: Color(0xFF32A060).withOpacity(0.2), + spreadRadius: 0, + blurRadius: 4, + offset: Offset(0, 4), // changes position of shadow + ), + ], + ), + child: Text( + "添加", + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight:MyFontWeight.regular), + ), ), ) ]), ); } + ///好友验证弹窗 + showFriendVerificationDialog() { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, // 移除默认内边距 + content: Container( + // width: MediaQuery.of(context).size.width - 84, + height: 130.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding(padding:EdgeInsets.symmetric(vertical:16.h), + child: Text( + "好友验证", + style: TextStyle( + color: Color(0xFF060606), + fontSize: 16.sp, + fontWeight: MyFontWeight.bold, + ), + ),), + Expanded(child:Text( + "我是秀才8856", + style: TextStyle( + color: Color(0xFF060606), + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + )), + // Spacer(), + Container( + margin:EdgeInsets.only(top:18.h), + height:1.h, + width: double.infinity, + color:Color(0xFFEDEDED), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + // height: 38.h, + child: Text( + "取消", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF060606), + ) + ) + ) + ) + ), + Container( + height:45, + width: 1.w, + color: Color(0xFFEDEDED), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + // height: 38.h, + child: Text( + "确认", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFF060606), + ) + ) + ) + ) + ) + ], + ) + ], + ), + ), + ); + }, + ); + } + /// 搜索框 Widget friendGroupSearch() { return Container( diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index c1323852..b89bf427 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -17,6 +17,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; +import '../retrofit/data/im_user_list.dart'; +import '../view_widget/custom_image.dart'; import 'im_view/on_chat_message.dart'; import 'im_view/on_chat_msg_instance.dart'; @@ -54,6 +56,8 @@ class _ChatDetailsPage extends State final ScrollController scrollController = ScrollController(); String tex = ""; int copyIndex = 0; + String selfUserId = ""; + ImUserList _toUser; @override void onMessage(txt) { @@ -64,11 +68,9 @@ class _ChatDetailsPage extends State loadMessageList() async { selfUserId = (await SharedPreferences.getInstance()).getString("userId"); + messages = await hxDatabase.queryUList(_toUser.mid); - toUserId = widget.arguments["toId"]; - messages = await hxDatabase.queryUList(toUserId); - - socketClient.addCallback(toUserId, (Message message) { + socketClient.addCallback(_toUser.mid, (Message message) { messages.add(message); refreshState(); @@ -80,12 +82,11 @@ class _ChatDetailsPage extends State if (mounted) setState(() {}); } - String selfUserId = ""; - String toUserId = ""; - @override void initState() { super.initState(); + + _toUser = widget.arguments["toUser"]; OnChatMsgInstance.instance.onChatMessage = this; WidgetsBinding.instance.addObserver(this); commentFocus.addListener(_focusNodeListener); @@ -154,7 +155,7 @@ class _ChatDetailsPage extends State WidgetsBinding.instance.removeObserver(this); commentFocus.removeListener(_focusNodeListener); - socketClient.removeCallback(toUserId); + socketClient.removeCallback(_toUser.mid); } @@ -250,7 +251,7 @@ class _ChatDetailsPage extends State // resizeToAvoidBottomInset: false, backgroundColor: Color(0xFFF6F6F6), appBar: MyAppBar( - title: "哈哈哈哈", + title: _toUser.nickname, titleColor: Color(0xFF0D0D0D), titleSize: 17.sp, leading: true, @@ -367,6 +368,7 @@ class _ChatDetailsPage extends State height: 16.h, ), if (copyIndex == 1) + ///复制聊天消息 Stack( alignment: Alignment.bottomCenter, children: [ @@ -456,19 +458,14 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", + MImage( + _toUser.avatar, + isCircle: true, height: 44.h, width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, @@ -658,19 +655,14 @@ class _ChatDetailsPage extends State SizedBox( width: 12.w, ), - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44, - width: 44, + MImage( + "", + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), ], ), @@ -682,19 +674,14 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 17.w, right: 39.w, top: 20.h), child: Row( children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", + MImage( + _toUser.avatar, + isCircle: true, height: 44.h, width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, @@ -786,7 +773,7 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient.sendMessage(toUserId, commentText).then((value) { + socketClient.sendMessage(_toUser.mid, commentText).then((value) { Message message = value; messages.insert(0, message); chatController.clear(); diff --git a/lib/im/database/contact.dart b/lib/im/database/contact.dart deleted file mode 100644 index 559e7b2a..00000000 --- a/lib/im/database/contact.dart +++ /dev/null @@ -1,33 +0,0 @@ -class Contact { - int id; - - String userId; - - String nickName; - - String imageUrl; - - int state; - - int isDelete; - - Contact(this.id, this.userId, this.nickName, this.imageUrl, this.state, - this.isDelete); - - factory Contact.fromJson(Map json) => Contact( - json["id"], - json["userId"], - json["nickName"], - json["imageUrl"], - json["state"], - json["isDelete"]); - - Map toJson() => { - "id": id, - "fromId": userId, - "toId": nickName, - "replyId": imageUrl, - "state": state, - "isDelete": isDelete == null ? 0 : isDelete - }; -} diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index b74b739a..1e1d59ad 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -1,17 +1,14 @@ - import 'package:flutter/cupertino.dart'; -import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; import 'package:sqflite/sqflite.dart'; +import '../../retrofit/data/im_user_list.dart'; class HxDatabase { - Database db; void open({String key}) async { - // _migrations.add(Migration(1, 2, (Database database) async { // database.execute('ALTER TABLE `Message` ADD COLUMN `replyId` VARCHAR(20) DEFAULT NULL AFTER `toId`'); // })); @@ -20,23 +17,19 @@ class HxDatabase { if (key?.isNotEmpty ?? false) { databaseName = 'hx_$key.db'; } - await openDatabase( - databaseName, - version: 2, + await openDatabase(databaseName, version: 2, onCreate: (Database db, int version) async { - db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); - db.execute('CREATE TABLE IF NOT EXISTS `Contact` (`id` INTEGER, `userId` VARCHAR(20), `nickName` VARCHAR(20), `imageUrl` VARCHAR(200), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); - }, - onConfigure: (database) async { - await database.execute('PRAGMA foreign_keys = ON'); - }, - onUpgrade: (database, startVersion, endVersion) async { - await runMigrations(database, startVersion, endVersion, _migrations); - }, - onOpen: (Database db) { - this.db = db; - } - ); + db.execute( + 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + db.execute( + 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, PRIMARY KEY (`id`))'); + }, onConfigure: (database) async { + await database.execute('PRAGMA foreign_keys = ON'); + }, onUpgrade: (database, startVersion, endVersion) async { + await runMigrations(database, startVersion, endVersion, _migrations); + }, onOpen: (Database db) { + this.db = db; + }); } void close() { @@ -47,8 +40,10 @@ class HxDatabase { if (db == null) { return Future.value(); } - String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; - List messages = await db.rawQuery(sql, [userId, userId]).then((value) { + String sql = + 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; + List messages = + await db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) { debugPrint("Message: ${e}"); return Message.fromJson(e); @@ -63,7 +58,8 @@ class HxDatabase { if (db == null) { return Future.value([]); } - String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? GROUP BY toId,fromId ORDER BY time DESC'; + String sql = + 'SELECT * FROM Message WHERE toId = ? OR fromId = ? GROUP BY toId,fromId ORDER BY time DESC'; return db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) { debugPrint("Message: ${e}"); @@ -78,7 +74,8 @@ class HxDatabase { if (db == null) { return Future.value([]); } - String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC'; + String sql = + 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC'; return db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { @@ -103,7 +100,8 @@ class HxDatabase { return Future.value(0); } debugPrint("Message_insert: $message"); - return db.update("Message", message, where: 'id = ?', whereArgs: [message['id']]); + return db.update("Message", message, + where: 'id = ?', whereArgs: [message['id']]); } Future insert(Map message) async { @@ -114,37 +112,41 @@ class HxDatabase { return db.insert("Message", message); } - Future insertContact(Map contactMap) async { + Future insertOrUpdateImUser(Map imUserMap) async { if (db == null) { return Future.value(0); } - debugPrint("contact_insert: $contactMap"); - return db.insert("Contact", contactMap); + debugPrint("imUser_insert: $imUserMap"); + if ((await queryImUserById(imUserMap['mid'])) == null) + return db.insert("ImUser", imUserMap); + else + return db.update("ImUser", imUserMap, + where: 'mid = ?', whereArgs: [imUserMap['mid']]); } - Future> queryContact(List userIds) async { + Future> queryImUser(List userIds) async { if (db == null) { - return Future.value([]); + return Future.value([]); } - String sql = 'SELECT * FROM Contact WHERE userId IN (?) AND state = 0 AND isDelete = 0'; - return db.rawQuery(sql, userIds).then((value) { - return value.map((e) => Contact.fromJson(e)).toList(); + String query = 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; + return db.rawQuery(query).then((value) { + return value.map((e) => ImUserList.fromJson(e)).toList(); }, onError: (error) { - debugPrint("Contact_error: $error"); + debugPrint("ImUser_error: $error"); }); } - Future queryContactById(String userId) async { + Future queryImUserById(String userId) async { if (db == null) { return Future.value(); } - List contact = await db.query("Contact", distinct: true, where: "userId = ?", whereArgs: [userId]) - .then((value) { - return value.map((e) => Contact.fromJson(e)).toList(); + List imUser = await db.query("ImUser", + distinct: true, where: "mid = ?", whereArgs: [userId]).then((value) { + return value.map((e) => ImUserList.fromJson(e)).toList(); }, onError: (error) { - debugPrint("Contact_error: $error"); + debugPrint("ImUser_error: $error"); }); - return (contact?.isNotEmpty ?? false) ? contact.first : null; + return (imUser?.isNotEmpty ?? false) ? imUser.first : null; } final List _migrations = []; @@ -155,22 +157,22 @@ class HxDatabase { } Future runMigrations( - final Database migrationDatabase, - final int startVersion, - final int endVersion, - final List migrations, - ) async { + final Database migrationDatabase, + final int startVersion, + final int endVersion, + final List migrations, + ) async { final relevantMigrations = migrations .where((migration) => migration.startVersion >= startVersion) .toList() ..sort( - (first, second) => first.startVersion.compareTo(second.startVersion)); + (first, second) => first.startVersion.compareTo(second.startVersion)); if (relevantMigrations.isEmpty || relevantMigrations.last.endVersion != endVersion) { throw StateError( 'There is no migration supplied to update the database to the current version.' - ' Aborting the migration.', + ' Aborting the migration.', ); } @@ -178,5 +180,4 @@ class HxDatabase { await migration.migrate(migrationDatabase); } } - } diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 1e0b5d22..8f54f17e 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -4,9 +4,10 @@ import 'dart:collection'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/generated/l10n.dart'; -import 'package:huixiang/im/database/contact.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/im/im_view/time_formatter.dart'; import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; @@ -17,9 +18,13 @@ import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_footer.dart'; import 'package:huixiang/view_widget/round_button.dart'; +import 'package:intl/intl.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../retrofit/data/im_user_list.dart'; +import '../../utils/flutter_utils.dart'; +import '../../view_widget/custom_image.dart'; import 'on_chat_message.dart'; import 'on_chat_msg_instance.dart'; @@ -36,6 +41,7 @@ class _IMPage extends State implements OnChatMessage { ApiService apiService; int pageNum = 1; List messages = []; + Map imUserList = {}; Map msgNumber = { "1": 0, "2": 0, @@ -67,7 +73,8 @@ class _IMPage extends State implements OnChatMessage { loadMessageList(); SharedPreferences.getInstance().then((value) { - apiService = ApiService(Dio(), token: value.getString("token"), context: context); + apiService = + ApiService(Dio(), token: value.getString("token"), context: context); queryMsgStats(); }); } @@ -79,9 +86,8 @@ class _IMPage extends State implements OnChatMessage { } List userIds = []; - Map contactMap = {}; Map lastMessageMap = {}; - Stream streamSubscription ; + Stream streamSubscription; loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); @@ -97,9 +103,9 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap[message.fromId] = message; debugPrint("messages_records : ${message.toJson()}"); - if (contactMap[message.fromId] == null) { - /// TODO: message.fromId request Api and setState - } + // if (contactMap[message.fromId] == null) { + // /// TODO: message.fromId request Api and setState + // } if (mounted) { setState(() {}); } @@ -112,23 +118,20 @@ class _IMPage extends State implements OnChatMessage { }); userIds = messages .map((e) => e.toId != userId ? e.toId : e.fromId) - .toSet().where((element) => element != userId) - .toList(); + .toSet() + .where((element) => element != userId) + .toList(); //先处理列表的时间,我搞了一个 + List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; - lastMessageMap = groupBy(messages, (p0) => p0.toId != userId ? p0.toId : p0.fromId); + if (contacts.isNotEmpty) imUserList = groupBy(contacts, (p0) => p0.mid); - List contacts = await hxDatabase.queryContact(userIds); - if (contacts?.isEmpty ?? false) { - /// TODO: userIds request Api - } else { - List queryUserIds = userIds.where((u) => contacts.where((c) => c.userId == u).isEmpty).toList(); - /// TODO: queryUserIds request Api - } - contactMap = groupBy(contacts, (p0) => p0.userId); + lastMessageMap = + groupBy(messages, (p0) => p0.toId != userId ? p0.toId : p0.fromId); if (mounted) { setState(() {}); } + queryMemberInfo(userIds); } void updateLastMessage(String userId) async { @@ -211,6 +214,25 @@ class _IMPage extends State implements OnChatMessage { EasyLoading.dismiss(); } + ///批量查询用户信息 + queryMemberInfo(List mids) async { + BaseData> baseData = await apiService.memberInfoByIds({ + "mids": mids, + }).catchError((error) { + SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), + alignment: Alignment.center); + }); + if (baseData != null && baseData.isSuccess) { + baseData.data.forEach((element) async { + await hxDatabase.insertOrUpdateImUser(element.toJson()); + }); + imUserList = groupBy(baseData.data, (p0) => p0.mid); + setState(() {}); + } else { + SmartDialog.showToast(baseData.msg, alignment: Alignment.center); + } + } + RefreshController _refreshController = RefreshController(); @override @@ -280,7 +302,8 @@ class _IMPage extends State implements OnChatMessage { behavior: HitTestBehavior.opaque, onTap: () { Navigator.of(context) - .pushNamed('/router/chat_friend_group').then((value) { + .pushNamed('/router/chat_friend_group') + .then((value) { _refresh(); }); }, @@ -375,12 +398,13 @@ class _IMPage extends State implements OnChatMessage { behavior: HitTestBehavior.opaque, onTap: () { Navigator.of(context).pushNamed( - '/router/chat_details_page', + '/router/chat_details_page', arguments: { - "toId": userIds[position], + "toUser": imUserList[userIds[position]], }, ).then((value) { updateLastMessage(userIds[position]); + _refresh(); }); }, child: chatItem(userIds[position]), @@ -395,19 +419,16 @@ class _IMPage extends State implements OnChatMessage { padding: EdgeInsets.only(left: 16.w, right: 17.w, bottom: 18.h), child: Row( children: [ - // MImage( - // "", - // isCircle: true, - // width: 40, - // height: 40, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", + MImage( + !imUserList.containsKey(userId) + ? null + : imUserList[userId]?.avatar ?? "", + isCircle: true, height: 54.h, width: 54.h, + fit: BoxFit.cover, + errorSrc: "assets/image/fuka_zj.webp", + fadeSrc: "assets/image/fuka_zj.webp", ), SizedBox( width: 12.w, @@ -420,7 +441,9 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - "喽哈 $userId", + !imUserList.containsKey(userId) + ? "" + : imUserList[userId]?.nickname ?? "", // overflow: TextOverflow.fade, maxLines: 1, style: TextStyle( @@ -431,7 +454,11 @@ class _IMPage extends State implements OnChatMessage { ), ), Text( - "2021.03.08 13:22", + lastMessageMap[userId]?.time != null + ? TimeFormatter.formatTime( + DateTime.fromMillisecondsSinceEpoch(num.parse( + lastMessageMap[userId]?.time ?? ""))) + : "", style: TextStyle( fontSize: 12.sp, color: Color(0xFFA29E9E), @@ -447,7 +474,7 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - "新开的火锅店好吃得很", + lastMessageMap[userId]?.content ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -482,5 +509,4 @@ class _IMPage extends State implements OnChatMessage { ), ); } - } diff --git a/lib/im/im_view/time_formatter.dart b/lib/im/im_view/time_formatter.dart new file mode 100644 index 00000000..c406c9a5 --- /dev/null +++ b/lib/im/im_view/time_formatter.dart @@ -0,0 +1,20 @@ +import 'package:intl/intl.dart'; + +class TimeFormatter { + static String formatTime(DateTime time) { + final now = DateTime.now(); + final diff = now.difference(time).inHours; + + if (diff < 24) { + return '刚刚'; // 24小时内显示为“刚刚” + } else if (diff < 48) { + return '昨天'; // 昨天 + } else if (diff < 72) { + return '前天'; // 前天 + } else if (time.year == now.year) { + return DateFormat('MM月dd日').format(time); // 今年内的日期 + } else { + return DateFormat('yyyy年MM月dd日').format(time); // 其他年份的日期 + } + } +} \ No newline at end of file diff --git a/lib/retrofit/data/im_user_list.dart b/lib/retrofit/data/im_user_list.dart new file mode 100644 index 00000000..8f59dd6d --- /dev/null +++ b/lib/retrofit/data/im_user_list.dart @@ -0,0 +1,61 @@ +/// mid : "1379254113602109440" +/// nickname : "哈哈" +/// avatar : "https://pos.upload.lotus-wallet.com/admin/2022/11/d501d2cd-ffc0-49f2-967c-2e463462f500.jpeg" +/// phone : "13052919193" +/// isFollow : null +/// createTime : null + +class ImUserList { + ImUserList({ + String mid, + String nickname, + num isDelete, + String avatar, + String phone, }){ + _mid = mid; + _nickname = nickname; + _isDelete = isDelete; + _avatar = avatar; + _phone = phone; +} + + ImUserList.fromJson(dynamic json) { + _mid = json['mid']; + _nickname = json['nickname']; + _isDelete = json['isDelete']; + _avatar = json['avatar']; + _phone = json['phone']; + } + String _mid; + String _nickname; + num _isDelete; + String _avatar; + String _phone; +ImUserList copyWith({ String mid, + String nickname, + num isDelete, + String avatar, + String phone, +}) => ImUserList( mid: mid ?? _mid, + nickname: nickname ?? _nickname, + isDelete: isDelete ?? _isDelete, + avatar: avatar ?? _avatar, + phone: phone ?? _phone, +); + String get mid => _mid; + String get nickname => _nickname; + num get isDelete => _isDelete; + String get avatar => _avatar; + String get phone => _phone; + + Map toJson() { + final map = {}; + map['mid'] = _mid; + map['nickname'] = _nickname; + map['isDelete'] = _isDelete; + map['avatar'] = _avatar; + map['phone'] = _phone; + return map; + } + +} \ No newline at end of file diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index 970d44c0..ceafabdc 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -39,6 +39,7 @@ import 'data/goods_category.dart'; import 'data/headlines_list.dart'; import 'data/headlines_list_details.dart'; import 'data/home_rank.dart'; +import 'data/im_user_list.dart'; import 'data/invitation_list.dart'; import 'data/invoice_list.dart'; import 'data/invoices_detail_info.dart'; @@ -653,4 +654,13 @@ abstract class ApiService { ///发票详情 @GET("invoice/detail{id}") Future> invoiceDetail(@Path("id") String id); + + ///消息页批量查询用户信息 + @POST("/member/memberInfoByIds") + Future>> memberInfoByIds(@Body() Map param); + + ///Im关键字搜索 + @GET("/member/memberSearch?keyword={keyword}") + Future>> memberSearch( + @Path("keyword") String keyword); } diff --git a/lib/retrofit/retrofit_api.g.dart b/lib/retrofit/retrofit_api.g.dart index 752b8278..15d0a63e 100644 --- a/lib/retrofit/retrofit_api.g.dart +++ b/lib/retrofit/retrofit_api.g.dart @@ -2528,4 +2528,52 @@ class _ApiService implements ApiService { ); return value; } + + @override + Future>> memberInfoByIds(param) async { + ArgumentError.checkNotNull(param, 'param'); + const _extra = {}; + final queryParameters = {}; + final _data = {}; + _data.addAll(param ?? {}); + final _result = await _dio.request>( + '/member/memberInfoByIds', + queryParameters: queryParameters, + options: RequestOptions( + method: 'POST', + headers: {}, + extra: _extra, + baseUrl: baseUrl), + data: _data); + final value = BaseData>.fromJson( + _result.data, + (json) => (json as List) + .map((i) => ImUserList.fromJson(i as Map)) + .toList()); + return value; + } + + @override + Future>> memberSearch(keyword) async { + ArgumentError.checkNotNull(keyword, 'keyword'); + const _extra = {}; + final queryParameters = {}; + final _data = {}; + final _result = await _dio.request>( + '/member/memberSearch?keyword=$keyword', + queryParameters: queryParameters, + options: RequestOptions( + method: 'GET', + headers: {}, + extra: _extra, + baseUrl: baseUrl), + data: _data); + final value = BaseData>.fromJson( + _result.data, + (json) => (json as List) + .map( + (i) => ImUserList.fromJson(i as Map)) + .toList()); + return value; + } } From d8d0296ac2ea31d7f9ecfcf58974793fb6501ceb Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 16:16:36 +0800 Subject: [PATCH 14/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 5 +++-- lib/im/im_view/im_page.dart | 40 ++++++++++++++++++++-------------- lib/retrofit/retrofit_api.dart | 1 + 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 4ec6a8e6..e346c559 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; +import 'package:huixiang/retrofit/data/im_user_list.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; @@ -152,7 +153,7 @@ class _ChatDetailsPage extends State WidgetsBinding.instance.removeObserver(this); commentFocus.removeListener(_focusNodeListener); - socketClient.removeCallback(toUserId); + socketClient.removeCallback(_toUser.mid); } @@ -784,7 +785,7 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient.sendMessage(toUserId, commentText).then((value) { + socketClient.sendMessage(_toUser.mid, commentText).then((value) { Message message = value; messages.insert(0, message); chatController.clear(); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index d6a8cae3..b14a4687 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -5,6 +5,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/im_view/time_formatter.dart'; @@ -41,7 +42,6 @@ class _IMPage extends State implements OnChatMessage { ApiService apiService; int pageNum = 1; List messages = []; - Map imUserList = {}; Map msgNumber = { "1": 0, "2": 0, @@ -87,9 +87,8 @@ class _IMPage extends State implements OnChatMessage { List userIds = []; Map lastMessageMap = {}; - Stream streamSubscription; Map unreadCountMap = {}; - Stream streamSubscription ; + Map contactMap = {}; loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); @@ -105,9 +104,10 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap[message.fromId] = message; debugPrint("messages_records : ${message.toJson()}"); - // if (contactMap[message.fromId] == null) { - // /// TODO: message.fromId request Api and setState - // } + if (contactMap[message.fromId] == null) { + queryMemberInfo([message.fromId]); + return; + } if (mounted) { setState(() {}); } @@ -126,15 +126,23 @@ class _IMPage extends State implements OnChatMessage { List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); - if (contacts.isNotEmpty) imUserList = groupBy(contacts, (p0) => p0.mid); + lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; - lastMessageMap = - groupBy(messages, (p0) => p0.toId != userId ? p0.toId : p0.fromId); + if (contacts?.isEmpty ?? true) { + await queryMemberInfo(userIds); + return; + } else { + List queryUserIds = userIds.where((u) => contacts.where((c) => c.mid == u).isEmpty).toList(); + if (queryUserIds.isNotEmpty) { + await queryMemberInfo(queryUserIds); + return; + } + } + contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem; if (mounted) { setState(() {}); } - queryMemberInfo(userIds); } void updateLastMessage(String userId) async { @@ -234,7 +242,7 @@ class _IMPage extends State implements OnChatMessage { baseData.data.forEach((element) async { await hxDatabase.insertOrUpdateImUser(element.toJson()); }); - imUserList = groupBy(baseData.data, (p0) => p0.mid); + contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem; setState(() {}); } else { SmartDialog.showToast(baseData.msg, alignment: Alignment.center); @@ -407,7 +415,7 @@ class _IMPage extends State implements OnChatMessage { Navigator.of(context).pushNamed( '/router/chat_details_page', arguments: { - "toUser": imUserList[userIds[position]], + "toUser": contactMap[userIds[position]], }, ).then((value) { updateLastMessage(userIds[position]); @@ -427,9 +435,9 @@ class _IMPage extends State implements OnChatMessage { child: Row( children: [ MImage( - !imUserList.containsKey(userId) + !contactMap.containsKey(userId) ? null - : imUserList[userId]?.avatar ?? "", + : contactMap[userId]?.avatar ?? "", isCircle: true, height: 54.h, width: 54.h, @@ -448,9 +456,9 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - !imUserList.containsKey(userId) + !contactMap.containsKey(userId) ? "" - : imUserList[userId]?.nickname ?? "", + : contactMap[userId]?.nickname ?? "", // overflow: TextOverflow.fade, maxLines: 1, style: TextStyle( diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index ceafabdc..5b095a30 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -663,4 +663,5 @@ abstract class ApiService { @GET("/member/memberSearch?keyword={keyword}") Future>> memberSearch( @Path("keyword") String keyword); + } From 6ea93117e3428690171a67e5a2a6da306c66a628 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 16:37:58 +0800 Subject: [PATCH 15/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/database/hx_database.dart | 1 + lib/im/im_view/im_page.dart | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 8c576253..f8ca35c1 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; import 'package:sqflite/sqflite.dart'; diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index b14a4687..b5e9bafe 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -232,6 +232,9 @@ class _IMPage extends State implements OnChatMessage { ///批量查询用户信息 queryMemberInfo(List mids) async { + if (mids.isEmpty) { + return; + } BaseData> baseData = await apiService.memberInfoByIds({ "mids": mids, }).catchError((error) { @@ -239,17 +242,19 @@ class _IMPage extends State implements OnChatMessage { alignment: Alignment.center); }); if (baseData != null && baseData.isSuccess) { - baseData.data.forEach((element) async { - await hxDatabase.insertOrUpdateImUser(element.toJson()); - }); - contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem; - setState(() {}); + if (baseData.data.isNotEmpty) { + baseData.data.forEach((element) async { + await hxDatabase.insertOrUpdateImUser(element.toJson()); + }); + contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem; + setState(() {}); + } } else { SmartDialog.showToast(baseData.msg, alignment: Alignment.center); } } - RefreshController _refreshController = RefreshController(); + final RefreshController _refreshController = RefreshController(); @override Widget build(BuildContext context) { From bf1345b13c4941e81ac46a593e4f888f223148fe Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 17:00:30 +0800 Subject: [PATCH 16/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 7 +++- lib/im/database/hx_database.dart | 14 +++---- lib/im/im_view/im_page.dart | 39 ++++++++++--------- .../data/{im_user_list.dart => im_user.dart} | 10 ++--- lib/retrofit/retrofit_api.dart | 6 +-- lib/retrofit/retrofit_api.g.dart | 14 +++---- 6 files changed, 47 insertions(+), 43 deletions(-) rename lib/retrofit/data/{im_user_list.dart => im_user.dart} (89%) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index e346c559..05b8c9b5 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -9,7 +9,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; -import 'package:huixiang/retrofit/data/im_user_list.dart'; +import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; @@ -56,7 +56,7 @@ class _ChatDetailsPage extends State String tex = ""; int copyIndex = 0; String selfUserId = ""; - ImUserList _toUser; + ImUser _toUser; @override void onMessage(txt) { @@ -66,7 +66,10 @@ class _ChatDetailsPage extends State List messages = []; loadMessageList() async { + selfUserId = (await SharedPreferences.getInstance()).getString("userId"); + // unread msg 2 read state + await hxDatabase.readMessage(selfUserId, _toUser.mid); messages = await hxDatabase.queryUList(_toUser.mid); socketClient.addCallback(_toUser.mid, (Message message) { diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index f8ca35c1..7d4cbfc1 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -4,7 +4,7 @@ import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; import 'package:sqflite/sqflite.dart'; -import '../../retrofit/data/im_user_list.dart'; +import '../../retrofit/data/im_user.dart'; class HxDatabase { Database db; @@ -147,25 +147,25 @@ class HxDatabase { where: 'mid = ?', whereArgs: [imUserMap['mid']]); } - Future> queryImUser(List userIds) async { + Future> queryImUser(List userIds) async { if (db == null) { - return Future.value([]); + return Future.value([]); } String query = 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; return db.rawQuery(query).then((value) { - return value.map((e) => ImUserList.fromJson(e)).toList(); + return value.map((e) => ImUser.fromJson(e)).toList(); }, onError: (error) { debugPrint("ImUser_error: $error"); }); } - Future queryImUserById(String userId) async { + Future queryImUserById(String userId) async { if (db == null) { return Future.value(); } - List imUser = await db.query("ImUser", + List imUser = await db.query("ImUser", distinct: true, where: "mid = ?", whereArgs: [userId]).then((value) { - return value.map((e) => ImUserList.fromJson(e)).toList(); + return value.map((e) => ImUser.fromJson(e)).toList(); }, onError: (error) { debugPrint("ImUser_error: $error"); }); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index b5e9bafe..455c2432 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -23,7 +23,7 @@ import 'package:intl/intl.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../retrofit/data/im_user_list.dart'; +import '../../retrofit/data/im_user.dart'; import '../../utils/flutter_utils.dart'; import '../../view_widget/custom_image.dart'; import 'on_chat_message.dart'; @@ -88,7 +88,7 @@ class _IMPage extends State implements OnChatMessage { List userIds = []; Map lastMessageMap = {}; Map unreadCountMap = {}; - Map contactMap = {}; + Map contactMap = {}; loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); @@ -123,7 +123,7 @@ class _IMPage extends State implements OnChatMessage { .toSet() .where((element) => element != userId) .toList(); - List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; + List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; @@ -235,7 +235,7 @@ class _IMPage extends State implements OnChatMessage { if (mids.isEmpty) { return; } - BaseData> baseData = await apiService.memberInfoByIds({ + BaseData> baseData = await apiService.memberInfoByIds({ "mids": mids, }).catchError((error) { SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), @@ -504,22 +504,23 @@ class _IMPage extends State implements OnChatMessage { ), ), ), - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Color(0xFFFF441A), - ), - child: RoundButton( - text: "99", - textColor: Colors.white, - fontWeight: MyFontWeight.regular, - backgroup: Color(0xFFFF441A), - fontSize: 10.sp, - radius: 100, + if (unreadCountMap[userId] != null && unreadCountMap[userId] > 0) + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + child: RoundButton( + text: "${unreadCountMap[userId]}", + textColor: Colors.white, + fontWeight: MyFontWeight.regular, + backgroup: Color(0xFFFF441A), + fontSize: 10.sp, + radius: 100, + ), ), - ), ], ), ], diff --git a/lib/retrofit/data/im_user_list.dart b/lib/retrofit/data/im_user.dart similarity index 89% rename from lib/retrofit/data/im_user_list.dart rename to lib/retrofit/data/im_user.dart index 8f59dd6d..a57953bb 100644 --- a/lib/retrofit/data/im_user_list.dart +++ b/lib/retrofit/data/im_user.dart @@ -5,8 +5,8 @@ /// isFollow : null /// createTime : null -class ImUserList { - ImUserList({ +class ImUser { + ImUser({ String mid, String nickname, num isDelete, @@ -19,7 +19,7 @@ class ImUserList { _phone = phone; } - ImUserList.fromJson(dynamic json) { + ImUser.fromJson(dynamic json) { _mid = json['mid']; _nickname = json['nickname']; _isDelete = json['isDelete']; @@ -31,12 +31,12 @@ class ImUserList { num _isDelete; String _avatar; String _phone; -ImUserList copyWith({ String mid, +ImUser copyWith({ String mid, String nickname, num isDelete, String avatar, String phone, -}) => ImUserList( mid: mid ?? _mid, +}) => ImUser( mid: mid ?? _mid, nickname: nickname ?? _nickname, isDelete: isDelete ?? _isDelete, avatar: avatar ?? _avatar, diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index 5b095a30..a7a07276 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -39,7 +39,7 @@ import 'data/goods_category.dart'; import 'data/headlines_list.dart'; import 'data/headlines_list_details.dart'; import 'data/home_rank.dart'; -import 'data/im_user_list.dart'; +import 'data/im_user.dart'; import 'data/invitation_list.dart'; import 'data/invoice_list.dart'; import 'data/invoices_detail_info.dart'; @@ -657,11 +657,11 @@ abstract class ApiService { ///消息页批量查询用户信息 @POST("/member/memberInfoByIds") - Future>> memberInfoByIds(@Body() Map param); + Future>> memberInfoByIds(@Body() Map param); ///Im关键字搜索 @GET("/member/memberSearch?keyword={keyword}") - Future>> memberSearch( + Future>> memberSearch( @Path("keyword") String keyword); } diff --git a/lib/retrofit/retrofit_api.g.dart b/lib/retrofit/retrofit_api.g.dart index 15d0a63e..52182e80 100644 --- a/lib/retrofit/retrofit_api.g.dart +++ b/lib/retrofit/retrofit_api.g.dart @@ -2530,7 +2530,7 @@ class _ApiService implements ApiService { } @override - Future>> memberInfoByIds(param) async { + Future>> memberInfoByIds(param) async { ArgumentError.checkNotNull(param, 'param'); const _extra = {}; final queryParameters = {}; @@ -2545,16 +2545,16 @@ class _ApiService implements ApiService { extra: _extra, baseUrl: baseUrl), data: _data); - final value = BaseData>.fromJson( + final value = BaseData>.fromJson( _result.data, (json) => (json as List) - .map((i) => ImUserList.fromJson(i as Map)) + .map((i) => ImUser.fromJson(i as Map)) .toList()); return value; } @override - Future>> memberSearch(keyword) async { + Future>> memberSearch(keyword) async { ArgumentError.checkNotNull(keyword, 'keyword'); const _extra = {}; final queryParameters = {}; @@ -2568,11 +2568,11 @@ class _ApiService implements ApiService { extra: _extra, baseUrl: baseUrl), data: _data); - final value = BaseData>.fromJson( + final value = BaseData>.fromJson( _result.data, (json) => (json as List) - .map( - (i) => ImUserList.fromJson(i as Map)) + .map( + (i) => ImUser.fromJson(i as Map)) .toList()); return value; } From b3ca32e88fcbcd8213d61342bb63d166181911dd Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Thu, 19 Sep 2024 18:04:58 +0800 Subject: [PATCH 17/81] =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=A1=86=E6=95=B0=E6=8D=AE=E6=9B=B4=E6=94=B9=EF=BC=9B=20?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=9B=B4=E6=94=B9=EF=BC=9B=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BB=9F=E4=B8=80=E6=90=9C=E7=B4=A2=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/add_friend.dart | 71 ++++++------- lib/im/chat_details_page.dart | 51 ++++----- lib/im/im_search.dart | 111 ++++++++++++++++++++ lib/im/im_view/friend_groip_list.dart | 144 ++++++++++++-------------- lib/im/im_view/im_page.dart | 89 +++++++--------- lib/main.dart | 3 + lib/mine/personal_page.dart | 13 ++- 7 files changed, 287 insertions(+), 195 deletions(-) diff --git a/lib/im/add_friend.dart b/lib/im/add_friend.dart index 71ca7307..3c01e717 100644 --- a/lib/im/add_friend.dart +++ b/lib/im/add_friend.dart @@ -18,8 +18,6 @@ class AddFriend extends StatefulWidget { class _AddFriend extends State { ApiService apiService; - final TextEditingController editingController = TextEditingController(); - FocusNode _focusNode = FocusNode(); @override void initState() { @@ -29,7 +27,6 @@ class _AddFriend extends State { ///离开页面记着销毁和清除 @override void dispose() { - _focusNode.unfocus(); super.dispose(); } @@ -70,7 +67,7 @@ class _AddFriend extends State { ) ], ),), - friendGroupSearch(), + addFriendSearch(), Padding( padding: EdgeInsets.only(left:18.w,bottom: 16.h), child: Text( @@ -250,42 +247,38 @@ class _AddFriend extends State { } /// 搜索框 - Widget friendGroupSearch() { - return Container( - margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,29.h), - padding: EdgeInsets.symmetric(vertical: 13.h), - decoration: BoxDecoration( - color: Color(0xFFFDFCFC), - borderRadius: BorderRadius.circular(4), - ), - child: TextField( - textInputAction: TextInputAction.search, - onEditingComplete: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - controller: editingController, - style: TextStyle( - fontSize: 14.sp, - ), - decoration: InputDecoration( - hintText: "搜索", - hintStyle: TextStyle( - fontSize: 14.sp, - color: Color(0xFFA29E9E), - ), - isCollapsed: true, - prefixIcon: Padding( - padding: EdgeInsets.only(left: 15.w, right: 5.w), - child: Image.asset( - "assets/image/icon_search.webp", - width: 14.h, - height: 14.h, - color: Color(0xFFB3B3B3), - ), + Widget addFriendSearch() { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){Navigator.of(context).pushNamed('/router/im_search');}, + child: Container( + margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + padding: EdgeInsets.symmetric(vertical: 13.h), + decoration: BoxDecoration( + color: Color(0xFFFDFCFC), + borderRadius: BorderRadius.circular(4), ), - prefixIconConstraints: BoxConstraints(), - border: InputBorder.none, - ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 15.w, right: 5.w), + child: Image.asset( + "assets/image/icon_search.webp", + width: 14.h, + height: 14.h, + color: Color(0xFF353535), + ), + ), + Text( + "搜索", + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 16.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ) ), ); } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 05b8c9b5..da5ca5b2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -18,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; +import '../view_widget/custom_image.dart'; import 'im_view/on_chat_message.dart'; import 'im_view/on_chat_msg_instance.dart'; @@ -155,9 +156,8 @@ class _ChatDetailsPage extends State OnChatMsgInstance.instance.onChatMessage = _tempOnChatMessage; WidgetsBinding.instance.removeObserver(this); commentFocus.removeListener(_focusNodeListener); - + scrollController.dispose(); socketClient.removeCallback(_toUser.mid); - } void _focusNodeListener() { @@ -252,7 +252,7 @@ class _ChatDetailsPage extends State // resizeToAvoidBottomInset: false, backgroundColor: Color(0xFFF6F6F6), appBar: MyAppBar( - title: "哈哈哈哈", + title: _toUser.nickname, titleColor: Color(0xFF0D0D0D), titleSize: 17.sp, leading: true, @@ -453,24 +453,19 @@ class _ChatDetailsPage extends State ), /// not self - if (!isSelf && isText) + if (!isSelf && !isText) Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", + MImage( + _toUser.avatar, + isCircle: true, height: 44.h, width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, @@ -660,25 +655,25 @@ class _ChatDetailsPage extends State SizedBox( width: 12.w, ), - // MImage( - // "", - // isCircle: true, - // width: 44, + MImage( + _toUser.avatar, + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + ), + // Image.asset( + // "assets/image/fuka_zj.webp", // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", + // width: 44, // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44, - width: 44, - ), ], ), ), - /// not self image + /// not self image 图片 if (!isSelf && !isText) Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w, top: 20.h), diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index e69de29b..47205b71 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -0,0 +1,111 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../retrofit/retrofit_api.dart'; +import '../view_widget/my_appbar.dart'; + + +class ImSearch extends StatefulWidget { + @override + State createState() { + return _ImSearch(); + } +} + +class _ImSearch extends State { + ApiService apiService; + final TextEditingController editingController = TextEditingController(); + FocusNode _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + } + + ///离开页面记着销毁和清除 + @override + void dispose() { + _focusNode.unfocus(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap:(){ + FocusScope.of(context).requestFocus(FocusNode()); + }, + child: Scaffold( + backgroundColor: Color(0xFFFFFFFF), + resizeToAvoidBottomInset: false, + appBar: MyAppBar( + title: "", + leading: true, + leadingColor: Colors.black, + background: Color(0xFFFFFFFF), + ), + body: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + imSearch(), + // Expanded( + // child: ListView.builder( + // itemCount: 10, + // physics: BouncingScrollPhysics(), + // shrinkWrap: true, + // itemBuilder: (context, position) { + // return imSearch(); + // }, + // )), + ], + ), + ), + ), + ); + } + + ///搜索列表 + Widget imSearch() { + return Container( + margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,29.h), + padding: EdgeInsets.symmetric(vertical: 13.h), + decoration: BoxDecoration( + color: Color(0xFFFDFCFC), + borderRadius: BorderRadius.circular(4), + ), + child: TextField( + textInputAction: TextInputAction.search, + onEditingComplete: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + controller: editingController, + style: TextStyle( + fontSize: 14.sp, + ), + decoration: InputDecoration( + hintText: "搜索", + hintStyle: TextStyle( + fontSize: 14.sp, + color: Color(0xFFA29E9E), + ), + isCollapsed: true, + prefixIcon: Padding( + padding: EdgeInsets.only(left: 15.w, right: 5.w), + child: Image.asset( + "assets/image/icon_search.webp", + width: 14.h, + height: 14.h, + color: Color(0xFFB3B3B3), + ), + ), + prefixIconConstraints: BoxConstraints(), + border: InputBorder.none, + ), + ), + ); + } +} diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index cfdbfed8..cb4b342f 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../retrofit/data/base_data.dart'; @@ -10,6 +11,7 @@ import '../../retrofit/data/page.dart'; import '../../retrofit/retrofit_api.dart'; import 'package:huixiang/im/database/message.dart'; +import '../../utils/font_weight.dart'; import '../../view_widget/custom_image.dart'; import '../../view_widget/no_data_view.dart'; @@ -17,9 +19,7 @@ class FriendGroupList extends StatefulWidget { final String isMyFans; final String title; - FriendGroupList( - Key key,this.isMyFans,this.title - ) : super(key: key); + FriendGroupList(Key key, this.isMyFans, this.title) : super(key: key); @override State createState() { @@ -29,9 +29,7 @@ class FriendGroupList extends StatefulWidget { class _FriendGroupList extends State { ApiService apiService; - final TextEditingController editingController = TextEditingController(); RefreshController _refreshController; - FocusNode _focusNode = FocusNode(); List userIds = []; List messages = []; int pageNum = 1; @@ -41,9 +39,9 @@ class _FriendGroupList extends State { void initState() { super.initState(); // loadMessageList(); - if( widget.isMyFans == ""){ + if (widget.isMyFans == "") { queryMutualFollowList(); - } else{ + } else { queryFollowList(); } } @@ -51,7 +49,6 @@ class _FriendGroupList extends State { ///离开页面记着销毁和清除 @override void dispose() { - _focusNode.unfocus(); super.dispose(); } @@ -95,14 +92,11 @@ class _FriendGroupList extends State { list.clear(); } list.addAll(baseData.data.list); - if(!mounted) - return; + if (!mounted) return; setState(() {}); } } - - ///好友列表 queryMutualFollowList() async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); @@ -126,8 +120,7 @@ class _FriendGroupList extends State { list.clear(); } list.addAll(baseData.data.list); - if(!mounted) - return; + if (!mounted) return; setState(() {}); } } @@ -141,32 +134,37 @@ class _FriendGroupList extends State { friendGroupSearch(), (list == null || list.length == 0) ? NoDataView( - src: "assets/image/guan_zhu.webp", - isShowBtn: false, - text: widget.title == "好友"?"目前暂无${widget?.title ?? ""}":("目前暂无${widget?.title ?? ""},${widget?.title == "粉丝" ? - "听说多发动态可以涨粉哦" :"可以在社群广场中关注自己喜欢的人哦"}~"), - fontSize: 16.sp, - margin: EdgeInsets.only(top: 120.h,left: 60.w,right: 60.w), - ):Expanded( - child: ListView.builder( - itemCount: list.length ?? 0, - physics: BouncingScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, position) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap:(){ - Navigator.of(context).pushNamed( - '/router/chat_details_page', - arguments: { - "toId":list[position].mid, - }, - ); - }, - child:friendGroupItem(list[position]), - ); - }, - )), + src: "assets/image/guan_zhu.webp", + isShowBtn: false, + text: widget.title == "好友" + ? "目前暂无${widget?.title ?? ""}" + : ("目前暂无${widget?.title ?? ""},${widget?.title == "粉丝" ? "听说多发动态可以涨粉哦" : "可以在社群广场中关注自己喜欢的人哦"}~"), + fontSize: 16.sp, + margin: EdgeInsets.only(top: 120.h, left: 60.w, right: 60.w), + ) + : Expanded( + child: ListView.builder( + itemCount: list.length ?? 0, + physics: BouncingScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, position) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toUser": ImUser( + avatar: list[position].avatar, + mid: list[position].mid, + nickname: list[position].nickname), + }, + ); + }, + child: friendGroupItem(list[position]), + ); + }, + )), ], ), ); @@ -174,7 +172,7 @@ class _FriendGroupList extends State { Widget friendGroupItem(ListData list) { return Container( - margin: EdgeInsets.only(left:16.w,right:16.w,bottom: 24.h), + margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 24.h), child: Row(children: [ Container( decoration: BoxDecoration( @@ -206,41 +204,37 @@ class _FriendGroupList extends State { /// 搜索框 Widget friendGroupSearch() { - return Container( - margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,24.h), - padding: EdgeInsets.symmetric(vertical: 13.h), - decoration: BoxDecoration( - color: Color(0xFFFDFCFC), - borderRadius: BorderRadius.circular(4), - ), - child: TextField( - textInputAction: TextInputAction.search, - onEditingComplete: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - controller: editingController, - style: TextStyle( - fontSize: 14.sp, - ), - decoration: InputDecoration( - hintText: "搜索", - hintStyle: TextStyle( - fontSize: 14.sp, - color: Color(0xFFA29E9E), - ), - isCollapsed: true, - prefixIcon: Padding( - padding: EdgeInsets.only(left: 15.w, right: 5.w), - child: Image.asset( - "assets/image/icon_search.webp", - width: 14.h, - height: 14.h, - color: Color(0xFFB3B3B3), - ), + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){Navigator.of(context).pushNamed('/router/im_search');}, + child: Container( + margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + padding: EdgeInsets.symmetric(vertical: 13.h), + decoration: BoxDecoration( + color: Color(0xFFFDFCFC), + borderRadius: BorderRadius.circular(4), ), - prefixIconConstraints: BoxConstraints(), - border: InputBorder.none, - ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 15.w, right: 5.w), + child: Image.asset( + "assets/image/icon_search.webp", + width: 14.h, + height: 14.h, + color: Color(0xFF353535), + ), + ), + Text( + "搜索", + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 16.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ) ), ); } diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 455c2432..322924ff 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -51,7 +51,10 @@ class _IMPage extends State implements OnChatMessage { "6": 0, }; int state = 0; - final TextEditingController imEditingController = TextEditingController(); + List userIds = []; + Map lastMessageMap = {}; + Map unreadCountMap = {}; + Map contactMap = {}; @override void onMessage(txt) { @@ -85,11 +88,6 @@ class _IMPage extends State implements OnChatMessage { queryMsgStats(); } - List userIds = []; - Map lastMessageMap = {}; - Map unreadCountMap = {}; - Map contactMap = {}; - loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); @@ -124,7 +122,6 @@ class _IMPage extends State implements OnChatMessage { .where((element) => element != userId) .toList(); List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; - unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; @@ -344,7 +341,7 @@ class _IMPage extends State implements OnChatMessage { ], ), ), - imSearchItem(), + imPageSearch(), chatList(), // buildMessage(),fgg ], @@ -357,50 +354,40 @@ class _IMPage extends State implements OnChatMessage { } ///搜索 - Widget imSearchItem() { - return Container( - margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), - padding: EdgeInsets.symmetric(vertical: 13.h), - decoration: BoxDecoration( - color: Color(0xFFFFFFFF), - borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(12), - offset: Offset(0, 3), - blurRadius: 14, - spreadRadius: 0, - ), - ], - ), - child: TextField( - textInputAction: TextInputAction.search, - onEditingComplete: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - controller: imEditingController, - style: TextStyle( - fontSize: 14.sp, - ), - decoration: InputDecoration( - hintText: "搜索", - hintStyle: TextStyle( - fontSize: 14.sp, - color: Color(0xFFA29E9E), - ), - isCollapsed: true, - prefixIcon: Padding( - padding: EdgeInsets.only(left: 15.w, right: 5.w), - child: Image.asset( - "assets/image/icon_search.webp", - width: 14.h, - height: 14.h, - color: Color(0xFF353535), - ), + Widget imPageSearch() { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){ + Navigator.of(context).pushNamed('/router/im_search'); + }, + child: Container( + margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + padding: EdgeInsets.symmetric(vertical: 13.h), + decoration: BoxDecoration( + color: Color(0xFFFDFCFC), + borderRadius: BorderRadius.circular(4), ), - prefixIconConstraints: BoxConstraints(), - border: InputBorder.none, - ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 15.w, right: 5.w), + child: Image.asset( + "assets/image/icon_search.webp", + width: 14.h, + height: 14.h, + color: Color(0xFF353535), + ), + ), + Text( + "搜索", + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 16.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ) ), ); } diff --git a/lib/main.dart b/lib/main.dart index 3eb87255..f3b09399 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -152,6 +152,7 @@ import 'im/chat_details_page.dart'; import 'im/chat_friend_group.dart'; import 'im/chat_setting.dart'; import 'im/contact_share.dart'; +import 'im/im_search.dart'; import 'login/login_store_select.dart'; import 'login/new_login_page.dart'; import 'login/phone_address_page.dart'; @@ -625,5 +626,7 @@ Map routers = { EditInvoicesInfo(arguments:arguments), '/router/invoices_detail_page': (context, {arguments}) => InvoicesDetailPage(arguments:arguments), + '/router/im_search': (context, {arguments}) => + ImSearch(), }; diff --git a/lib/mine/personal_page.dart b/lib/mine/personal_page.dart index 2202e9a6..4ed4873d 100644 --- a/lib/mine/personal_page.dart +++ b/lib/mine/personal_page.dart @@ -26,6 +26,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../retrofit/data/im_user.dart'; import '../view_widget/my_tab.dart'; class PersonalPage extends StatefulWidget { @@ -862,7 +863,12 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing behavior: HitTestBehavior.opaque, onTap: () { Navigator.of(context) - .pushNamed('/router/chat_details_page'); + .pushNamed('/router/chat_details_page',arguments:{ + "toUser": ImUser( + avatar: memberInfor?.headimg, + mid: memberInfor?.id, + nickname: memberInfor?.nickname) + }); }, child: Container( padding: @@ -1294,7 +1300,10 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing onTap: () { Navigator.of(context) .pushNamed('/router/chat_details_page', arguments: { - "toId": memberInfor.id, + "toUser": ImUser( + avatar: memberInfor?.headimg, + mid: memberInfor?.id, + nickname: memberInfor?.nickname) },); }, From c88cee55ff3f25cb6d3b61d4c2278d8d0636fb7d Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 18:09:20 +0800 Subject: [PATCH 18/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constant.dart | 24 +++++++++++++++++++++--- lib/im/chat_details_page.dart | 7 ++++--- lib/im/database/hx_database.dart | 9 +++++---- lib/im/im_view/im_page.dart | 13 ++++++------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/constant.dart b/lib/constant.dart index 169bf5db..3fb704e3 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -17,17 +17,29 @@ Map groupCount(Map> values) { return map; } -Map groupItem(Map> values) { +Map groupItem(Map> values, {int Function(T) key}) { var map = {}; for (var element in values.keys) { if (values[element] == null) { continue; } - map["$element"] = values[element].first; + map["$element"] = key == null ? values[element].first : values[element].lMax(key); } return map; } +T max(Iterable list, int Function(T) key) { + T tt; + for (T t in list) { + if (tt == null) { + tt = t; + } + if (key(tt) < key(t)) { + tt = t; + } + } + return tt; +} extension ListExtension on Iterable { @@ -35,12 +47,18 @@ extension ListExtension on Iterable { return groupBy(this, key); } + T lMax(int Function(T) key) { + return max(this, key); + } + } extension MapExtension on Map> { Map get mGroupCount => groupCount(this); - Map get mGroupItem => groupItem(this); + Map mGroupItem({int Function(T) key}) { + return groupItem(this, key: key); + } } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 05b8c9b5..a127db06 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -77,6 +77,10 @@ class _ChatDetailsPage extends State refreshState(); }); refreshState(); + + Future.delayed(const Duration(milliseconds: 1000), () { + jumpToBottom(); + }); } refreshState() { @@ -94,9 +98,6 @@ class _ChatDetailsPage extends State loadMessageList(); - Future.delayed(Duration.zero, () { - jumpToBottom(); - }); } void jumpToBottom() { diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 7d4cbfc1..e0338bb7 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -60,7 +60,7 @@ class HxDatabase { return Future.value([]); } String sql = - 'SELECT * FROM Message WHERE toId = ? OR fromId = ? GROUP BY toId,fromId ORDER BY time DESC'; + 'SELECT * FROM (SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC) mm GROUP BY mm.toId,mm.fromId'; return db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) { debugPrint("Message: ${e}"); @@ -71,13 +71,14 @@ class HxDatabase { }); } - Future> queryUList(userId) { + Future> queryUList(userId, {int page = 1, int pageSize = 10}) { if (db == null) { return Future.value([]); } + int start = (page - 1) * pageSize; String sql = - 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC'; - return db.rawQuery(sql, [userId, userId]).then((value) { + 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT ?, ?'; + return db.rawQuery(sql, [userId, userId, start, pageSize]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Messageerror: $error"); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 455c2432..69d630aa 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -115,9 +115,7 @@ class _IMPage extends State implements OnChatMessage { debugPrint("messages: queryList"); messages = await hxDatabase.queryList(userId); - messages.forEach((element) { - debugPrint("messages: ${element.toJson()}"); - }); + userIds = messages .map((e) => e.toId != userId ? e.toId : e.fromId) .toSet() @@ -126,7 +124,7 @@ class _IMPage extends State implements OnChatMessage { List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); - lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem; + lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); if (contacts?.isEmpty ?? true) { await queryMemberInfo(userIds); @@ -138,7 +136,7 @@ class _IMPage extends State implements OnChatMessage { return; } } - contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem; + contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem(); if (mounted) { setState(() {}); @@ -147,6 +145,7 @@ class _IMPage extends State implements OnChatMessage { void updateLastMessage(String userId) async { Message message = await hxDatabase.lastMessage(userId); + debugPrint("lastmessage: $userId ${message.content} ${message.toJson()}"); if (message != null) { lastMessageMap[userId] = message; refreshState(); @@ -246,7 +245,7 @@ class _IMPage extends State implements OnChatMessage { baseData.data.forEach((element) async { await hxDatabase.insertOrUpdateImUser(element.toJson()); }); - contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem; + contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem(); setState(() {}); } } else { @@ -423,8 +422,8 @@ class _IMPage extends State implements OnChatMessage { "toUser": contactMap[userIds[position]], }, ).then((value) { + unreadCountMap[userIds[position]] = 0; updateLastMessage(userIds[position]); - _refresh(); }); }, child: chatItem(userIds[position]), From 890178671001c0f72163965b413eceedc53fc21c Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 18:28:35 +0800 Subject: [PATCH 19/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 20 ++++++++++---------- lib/im/database/message.dart | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 9bab3c30..769a04a1 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -336,7 +336,7 @@ class _ChatDetailsPage extends State Widget chatDetailsItem(Message message) { bool isSelf = message.fromId == selfUserId; - bool isText = message.msgType == 0; + bool isText = message.msgType == 1; return Container( padding: EdgeInsets.only( top: 32.h, @@ -354,17 +354,17 @@ class _ChatDetailsPage extends State ), if (messages.indexOf(message) == (messages.length - 1)) Padding( - padding: EdgeInsets.only(top: 10.h, bottom: 24.h), - child: Text( - "在对方未回复或关注你之前,你只能发送一条信息", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, + padding: EdgeInsets.only(top: 10.h, bottom: 24.h), + child: Text( + "在对方未回复或关注你之前,你只能发送一条信息", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 10.sp, + fontWeight: MyFontWeight.regular, + ), ), ), - ), if (messages.indexOf(message) == (messages.length - 1)) SizedBox( height: 16.h, diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 92ed21ed..188003b4 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -56,7 +56,7 @@ createMessage(var toId, String content, {String attach, int msgType, userId, rep "replyId": replyId, "content": content, "attach": attach, - "msgType": msgType ?? 0, + "msgType": msgType ?? 1, "time": "${DateTime.now().millisecondsSinceEpoch}", "state": 0, "isDelete": 0 From c9e9335f287400ab6e852e38e90507a755ccba40 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 21:10:37 +0800 Subject: [PATCH 20/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constant.dart | 2 + lib/im/SocketClient.dart | 16 +++ lib/im/chat_details_page.dart | 234 ++++++++++++++++++------------- lib/im/database/hx_database.dart | 25 ++-- lib/im/im_view/im_page.dart | 140 ++++++++++-------- 5 files changed, 248 insertions(+), 169 deletions(-) diff --git a/lib/constant.dart b/lib/constant.dart index 3fb704e3..ca0bfd1a 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -41,6 +41,8 @@ T max(Iterable list, int Function(T) key) { return tt; } + + extension ListExtension on Iterable { Map> lGroupBy(S Function(T) key) { diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index ee2bd1e9..80c012d8 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:core'; import 'dart:core'; @@ -37,16 +38,29 @@ class SocketClient { }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); + }, onDone: () { + debugPrint("socket-listen-down: down"); }); authRequest(shared.getString("token")); }).catchError((error) { debugPrint("socket-connect-error: $error"); + reconnectTime = 1500; + _socket = null; reconnect(); }); } + heartbeat() { + Timer.periodic(const Duration(milliseconds: 3000), (timer) { + Uint8List data = utf8.encode(jsonEncode({"heartbeat": DateTime.now().millisecondsSinceEpoch})); + MsgData msgData = MsgData(to: "0", from: userId, type: MsgType.TEXT, data: data); + final proto2 = Proto(5, 1, msgData.writeToBuffer()); + _socket.add(proto2.toBytes()); + }); + } + int reconnectTime = 1500; reconnect() { @@ -68,6 +82,7 @@ class SocketClient { dispose() { if (_socket != null) { + _socket = null; _socket.close(); } } @@ -109,6 +124,7 @@ class SocketClient { checkSocket() { if (_socket == null) { + reconnectTime = 1500; reconnect(); return false; } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 769a04a1..4f254ed2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -67,11 +67,15 @@ class _ChatDetailsPage extends State List messages = []; loadMessageList() async { + ImUser imUser = await hxDatabase.queryImUserById(_toUser.mid); + if (imUser == null) { + await hxDatabase.insertOrUpdateImUser(_toUser.toJson()); + } selfUserId = (await SharedPreferences.getInstance()).getString("userId"); // unread msg 2 read state await hxDatabase.readMessage(selfUserId, _toUser.mid); - messages = await hxDatabase.queryUList(_toUser.mid); + messages = await hxDatabase.queryUList(_toUser.mid, pageSize: 100); socketClient.addCallback(_toUser.mid, (Message message) { messages.add(message); @@ -98,7 +102,6 @@ class _ChatDetailsPage extends State commentFocus.addListener(_focusNodeListener); loadMessageList(); - } void jumpToBottom() { @@ -259,21 +262,21 @@ class _ChatDetailsPage extends State leading: true, leadingColor: Colors.black, action: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - Navigator.of(context).pushNamed('/router/chat_setting'); - copyIndex = 0; - }); - }, - child: Container( - alignment: Alignment.center, - child: Icon( - Icons.more_horiz, - color: Colors.black, - size: 30, - ), + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + Navigator.of(context).pushNamed('/router/chat_setting'); + copyIndex = 0; + }); + }, + child: Container( + alignment: Alignment.center, + child: Icon( + Icons.more_horiz, + color: Colors.black, + size: 30, ), + ), ), ), body: Container( @@ -286,17 +289,17 @@ class _ChatDetailsPage extends State child: Column( children: [ GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - setState(() { - emojiShowing = false; - isKeyBoardShow = emojiShowing; - moreShow = false; - isKeyBoardShow = moreShow; - FocusScope.of(context).requestFocus(FocusNode()); - }); - }, - child: chatDetailsList(), + behavior: HitTestBehavior.translucent, + onTap: () { + setState(() { + emojiShowing = false; + isKeyBoardShow = emojiShowing; + moreShow = false; + isKeyBoardShow = moreShow; + FocusScope.of(context).requestFocus(FocusNode()); + }); + }, + child: chatDetailsList(), ), ], ), @@ -344,7 +347,7 @@ class _ChatDetailsPage extends State child: Column( children: [ Text( - "3月08日 上午 12:10", + "${DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))}", textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFA29E9E), @@ -352,23 +355,23 @@ class _ChatDetailsPage extends State fontWeight: MyFontWeight.regular, ), ), - if (messages.indexOf(message) == (messages.length - 1)) - Padding( - padding: EdgeInsets.only(top: 10.h, bottom: 24.h), - child: Text( - "在对方未回复或关注你之前,你只能发送一条信息", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ), - if (messages.indexOf(message) == (messages.length - 1)) - SizedBox( - height: 16.h, - ), + // if (messages.indexOf(message) == (messages.length - 1)) + // Padding( + // padding: EdgeInsets.only(top: 10.h, bottom: 24.h), + // child: Text( + // "在对方未回复或关注你之前,你只能发送一条信息", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Color(0xFFA29E9E), + // fontSize: 10.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ), + // if (messages.indexOf(message) == (messages.length - 1)) + // SizedBox( + // height: 16.h, + // ), if (copyIndex == 1) Stack( alignment: Alignment.bottomCenter, @@ -382,7 +385,8 @@ class _ChatDetailsPage extends State borderRadius: BorderRadius.circular(6), ), padding: EdgeInsets.symmetric( - horizontal: 32.w, vertical: 7.5.h, + horizontal: 32.w, + vertical: 7.5.h, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -454,13 +458,17 @@ class _ChatDetailsPage extends State ), /// not self - if (!isSelf && !isText) + if (!isSelf && isText) + SizedBox( + height: 10.h, + ), + if (!isSelf && isText) Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( children: [ MImage( - _toUser.avatar, + _toUser.avatar, isCircle: true, height: 44.h, width: 44.h, @@ -472,45 +480,48 @@ class _ChatDetailsPage extends State width: 12.w, ), Expanded( + child: Container( + alignment: Alignment.centerLeft, child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ) - ], + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, ), - padding: EdgeInsets.symmetric( - vertical: 8.h, horizontal: 12.w), - child: GestureDetector( - onLongPress: () { - setState(() { - copyIndex = 1; - }); - }, - child: Text( - tex = message.content, - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Color(0XFF0D0D0D), - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, - ), - ), - ))), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + child: GestureDetector( + onLongPress: () { + setState(() { + copyIndex = 1; + }); + }, + child: Text( + tex = message.content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Color(0XFF0D0D0D), + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), + ), + ), + ),), + ), ], ), ), - if (!isSelf && isText) - SizedBox( - height: 40.h, - ), + if (copyIndex == 1) Stack( alignment: Alignment.bottomCenter, @@ -595,6 +606,10 @@ class _ChatDetailsPage extends State ), /// self + if (isSelf && isText) + SizedBox( + height: 10.h, + ), if (isSelf && isText) Padding( padding: EdgeInsets.only(left: 36.w, right: 16.w), @@ -630,9 +645,19 @@ class _ChatDetailsPage extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Color(0xFF32A060), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, ), - padding: - EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), child: GestureDetector( onLongPress: () { setState(() { @@ -732,19 +757,19 @@ class _ChatDetailsPage extends State ), /// no reply long time - if (messages.indexOf(message) == 0) - Padding( - padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), - child: Text( - "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ), + // if (messages.indexOf(message) == 0) + // Padding( + // padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), + // child: Text( + // "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Color(0xFFA29E9E), + // fontSize: 10.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ), ], ), ); @@ -784,14 +809,21 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient.sendMessage(_toUser.mid, commentText).then((value) { + socketClient + .sendMessage(_toUser.mid, commentText) + .then((value) { Message message = value; messages.insert(0, message); chatController.clear(); - if (mounted) - setState(() {}); + if (mounted) setState(() {}); + + if (scrollController.position != null) { + double offset = scrollController.position.maxScrollExtent; + debugPrint("offset: $offset"); + scrollController.animateTo(offset + 50, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + } + }); - // widget.queryMemberComment(commentText); }, maxLines: 8, minLines: 1, diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index e0338bb7..2af0d7a8 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -38,7 +38,7 @@ class HxDatabase { } Future lastMessage(String userId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } String sql = @@ -56,7 +56,7 @@ class HxDatabase { } Future> queryList(userId) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } String sql = @@ -72,7 +72,7 @@ class HxDatabase { } Future> queryUList(userId, {int page = 1, int pageSize = 10}) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } int start = (page - 1) * pageSize; @@ -86,7 +86,7 @@ class HxDatabase { } Future> messageUnreadCount(List userIds, String selfUserId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value({}); } List messages = await db.query( @@ -102,7 +102,7 @@ class HxDatabase { } Future> queryListAll() { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } String sql = 'SELECT * FROM Message ORDER BY time DESC'; @@ -114,7 +114,7 @@ class HxDatabase { } update(Map message) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("Message_insert: $message"); @@ -123,21 +123,23 @@ class HxDatabase { } Future insert(Map message) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("Message_insert: $message"); return db.insert("Message", message); } + /// update message read state readMessage(String selfUserId, String userId) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } db.update("Message", {"state": 1}, where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", whereArgs: [userId, selfUserId]); } + Future insertOrUpdateImUser(Map imUserMap) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("imUser_insert: $imUserMap"); @@ -149,7 +151,7 @@ class HxDatabase { } Future> queryImUser(List userIds) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } String query = 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; @@ -161,7 +163,7 @@ class HxDatabase { } Future queryImUserById(String userId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } List imUser = await db.query("ImUser", @@ -204,4 +206,5 @@ class HxDatabase { await migration.migrate(migrationDatabase); } } + } diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 60c1e81d..fbf7c6a9 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -74,25 +74,17 @@ class _IMPage extends State implements OnChatMessage { super.initState(); OnChatMsgInstance.instance.onChatMessage = this; - loadMessageList(); + initSocketClient(); + SharedPreferences.getInstance().then((value) { - apiService = - ApiService(Dio(), token: value.getString("token"), context: context); - queryMsgStats(); + apiService = ApiService(Dio(), token: value.getString("token"), context: context); + // queryMsgStats(); }); } - _refresh() { - pageNum = 1; - loadMessageList(); - queryMsgStats(); - } - - loadMessageList() async { + initSocketClient() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); - - debugPrint("messages: loadMessageList"); socketClient.addCallback(userId, (Message message) { if (userIds.contains(message.fromId)) { userIds.remove(message.fromId); @@ -101,28 +93,62 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap[message.fromId] = message; + sortConversation(lastMessageMap); + debugPrint("messages_records : ${message.toJson()}"); if (contactMap[message.fromId] == null) { queryMemberInfo([message.fromId]); return; } - if (mounted) { - setState(() {}); - } + refreshState(); }); - debugPrint("messages: queryList"); + loadMessageList(); + } + + _refresh() { + pageNum = 1; + loadMessageList(); + // queryMsgStats(); + } + + loadMessageList() async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); messages = await hxDatabase.queryList(userId); - userIds = messages + lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); + + await sortConversation(lastMessageMap); + + await queryUnreadCount(userIds, userId); + + await queryImUserInfo(userIds); + + refreshState(); + } + + /// update conversation by time sort + sortConversation(lastMessageMap) async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); + List sortMessages = lastMessageMap.values.toList(); + sortMessages.sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time))); + userIds = sortMessages .map((e) => e.toId != userId ? e.toId : e.fromId) .toSet() .where((element) => element != userId) .toList(); - List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; + } + + /// update conversation unreadcount + queryUnreadCount(userIds, userId) async { unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); - lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); + } + /// update imuser info by mids + queryImUserInfo(userIds) async { + List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; if (contacts?.isEmpty ?? true) { await queryMemberInfo(userIds); return; @@ -134,17 +160,15 @@ class _IMPage extends State implements OnChatMessage { } } contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem(); - - if (mounted) { - setState(() {}); - } } + /// update one conversation last message ,and update conversation sort void updateLastMessage(String userId) async { Message message = await hxDatabase.lastMessage(userId); debugPrint("lastmessage: $userId ${message.content} ${message.toJson()}"); if (message != null) { lastMessageMap[userId] = message; + await sortConversation(lastMessageMap); refreshState(); } } @@ -197,34 +221,34 @@ class _IMPage extends State implements OnChatMessage { // } // } - queryMsgStats() async { - if (apiService == null) { - SharedPreferences value = await SharedPreferences.getInstance(); - apiService = ApiService( - Dio(), - context: context, - token: value.getString("token"), - ); - } - BaseData> baseData = await apiService.stats().catchError((onError) { - debugPrint("stats.error: $onError"); - }); - if (baseData != null && baseData.isSuccess) { - setState(() { - msgNumber.forEach((key, value) { - msgNumber[key] = 0; - }); - baseData.data.forEach((element) { - if (msgNumber.containsKey(element.name)) { - msgNumber[element.name] = element.number; - } - }); - }); - _refreshController.loadComplete(); - _refreshController.refreshCompleted(); - } - EasyLoading.dismiss(); - } + // queryMsgStats() async { + // if (apiService == null) { + // SharedPreferences value = await SharedPreferences.getInstance(); + // apiService = ApiService( + // Dio(), + // context: context, + // token: value.getString("token"), + // ); + // } + // BaseData> baseData = await apiService.stats().catchError((onError) { + // debugPrint("stats.error: $onError"); + // }); + // if (baseData != null && baseData.isSuccess) { + // setState(() { + // msgNumber.forEach((key, value) { + // msgNumber[key] = 0; + // }); + // baseData.data.forEach((element) { + // if (msgNumber.containsKey(element.name)) { + // msgNumber[element.name] = element.number; + // } + // }); + // }); + // _refreshController.loadComplete(); + // _refreshController.refreshCompleted(); + // } + // EasyLoading.dismiss(); + // } ///批量查询用户信息 queryMemberInfo(List mids) async { @@ -242,8 +266,12 @@ class _IMPage extends State implements OnChatMessage { baseData.data.forEach((element) async { await hxDatabase.insertOrUpdateImUser(element.toJson()); }); - contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem(); - setState(() {}); + baseData.data.forEach((element) { + if (contactMap[element.mid] == null) { + contactMap[element.mid] = element; + } + }); + refreshState(); } } else { SmartDialog.showToast(baseData.msg, alignment: Alignment.center); @@ -270,9 +298,7 @@ class _IMPage extends State implements OnChatMessage { controller: _refreshController, onRefresh: _refresh, onLoading: () { - setState(() { - _refresh(); - }); + _refresh(); }, child: Container( // color: Colors.white, From 5ffe534883225ab2108967b92b83c9569bc1a87a Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 19 Sep 2024 21:42:09 +0800 Subject: [PATCH 21/81] =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 2 ++ lib/im/chat_details_page.dart | 15 +++++++++++++-- lib/im/im_view/im_page.dart | 25 +++++++++++++++++-------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 80c012d8..683043da 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -44,6 +44,7 @@ class SocketClient { authRequest(shared.getString("token")); + heartbeat(); }).catchError((error) { debugPrint("socket-connect-error: $error"); reconnectTime = 1500; @@ -54,6 +55,7 @@ class SocketClient { heartbeat() { Timer.periodic(const Duration(milliseconds: 3000), (timer) { + debugPrint("heartbeat: ${DateTime.now().millisecondsSinceEpoch}"); Uint8List data = utf8.encode(jsonEncode({"heartbeat": DateTime.now().millisecondsSinceEpoch})); MsgData msgData = MsgData(to: "0", from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 4f254ed2..98eb5086 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -78,8 +78,17 @@ class _ChatDetailsPage extends State messages = await hxDatabase.queryUList(_toUser.mid, pageSize: 100); socketClient.addCallback(_toUser.mid, (Message message) { - messages.add(message); + messages.insert(0, message); refreshState(); + + if (scrollController.position != null) { + double offset = scrollController.position.maxScrollExtent; + debugPrint("offset: $offset"); + Future.delayed(const Duration(milliseconds: 500), () { + scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + }); + } + }); refreshState(); @@ -820,7 +829,9 @@ class _ChatDetailsPage extends State if (scrollController.position != null) { double offset = scrollController.position.maxScrollExtent; debugPrint("offset: $offset"); - scrollController.animateTo(offset + 50, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + Future.delayed(const Duration(milliseconds: 500), () { + scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + }); } }); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index fbf7c6a9..fe435583 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -93,14 +93,7 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap[message.fromId] = message; - sortConversation(lastMessageMap); - - debugPrint("messages_records : ${message.toJson()}"); - if (contactMap[message.fromId] == null) { - queryMemberInfo([message.fromId]); - return; - } - refreshState(); + listenerRefresh(message); }); loadMessageList(); @@ -112,6 +105,22 @@ class _IMPage extends State implements OnChatMessage { // queryMsgStats(); } + listenerRefresh(Message message) async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); + + await sortConversation(lastMessageMap); + + await queryUnreadCount(userIds, userId); + + debugPrint("messages_records : ${message.toJson()}"); + if (contactMap[message.fromId] == null) { + queryMemberInfo([message.fromId]); + return; + } + refreshState(); + } + loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); From 6711b8a07909452bc61f44c9dd920336fb51b13d Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 12:05:49 +0800 Subject: [PATCH 22/81] socketclient heartbeat --- lib/im/SocketClient.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 683043da..4ec2a8e0 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -54,11 +54,11 @@ class SocketClient { } heartbeat() { - Timer.periodic(const Duration(milliseconds: 3000), (timer) { + Timer.periodic(const Duration(milliseconds: 30000), (timer) { debugPrint("heartbeat: ${DateTime.now().millisecondsSinceEpoch}"); Uint8List data = utf8.encode(jsonEncode({"heartbeat": DateTime.now().millisecondsSinceEpoch})); - MsgData msgData = MsgData(to: "0", from: userId, type: MsgType.TEXT, data: data); - final proto2 = Proto(5, 1, msgData.writeToBuffer()); + MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); + final proto2 = Proto(3, 1, msgData.writeToBuffer()); _socket.add(proto2.toBytes()); }); } From 2ceab5a1de26d5f5eef4bc59aa31a081500b3c7b Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 20 Sep 2024 12:18:40 +0800 Subject: [PATCH 23/81] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/add_friend.dart | 2 +- lib/im/chat_friend_group.dart | 3 +- lib/im/im_search.dart | 142 ++++++++++++++++++++++++-- lib/im/im_view/friend_groip_list.dart | 4 +- lib/im/im_view/im_page.dart | 8 -- 5 files changed, 135 insertions(+), 24 deletions(-) diff --git a/lib/im/add_friend.dart b/lib/im/add_friend.dart index 3c01e717..780a92ec 100644 --- a/lib/im/add_friend.dart +++ b/lib/im/add_friend.dart @@ -252,7 +252,7 @@ class _AddFriend extends State { behavior: HitTestBehavior.opaque, onTap:(){Navigator.of(context).pushNamed('/router/im_search');}, child: Container( - margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + margin: EdgeInsets.fromLTRB(16.w,22.h, 16.w, 24.h), padding: EdgeInsets.symmetric(vertical: 13.h), decoration: BoxDecoration( color: Color(0xFFFDFCFC), diff --git a/lib/im/chat_friend_group.dart b/lib/im/chat_friend_group.dart index c4d54953..ccb49dc6 100644 --- a/lib/im/chat_friend_group.dart +++ b/lib/im/chat_friend_group.dart @@ -61,7 +61,7 @@ class _ChatFriendGroup extends State querySocialInfo() async { SharedPreferences value = await SharedPreferences.getInstance(); apiService = ApiService(Dio(), - context: context, token: value.getString("token"), showLoading: true); + context: context, token: value.getString("token"), showLoading: false); BaseData baseData = await apiService.socialInfo().catchError((onError) {}); @@ -70,7 +70,6 @@ class _ChatFriendGroup extends State infoNumber = baseData.data; }); } - EasyLoading.dismiss(); } @override diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index 47205b71..5768f20d 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -1,11 +1,19 @@ import 'dart:ui'; +import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../retrofit/data/base_data.dart'; +import '../retrofit/data/im_user.dart'; import '../retrofit/retrofit_api.dart'; +import '../utils/font_weight.dart'; import '../view_widget/my_appbar.dart'; +import '../view_widget/settlement_tips_dialog.dart'; class ImSearch extends StatefulWidget { @@ -19,6 +27,10 @@ class _ImSearch extends State { ApiService apiService; final TextEditingController editingController = TextEditingController(); FocusNode _focusNode = FocusNode(); + List searchUser = []; + int searchState = 0; + int textType = 0; + int searchUserIndex = 0; @override void initState() { @@ -32,6 +44,31 @@ class _ImSearch extends State { super.dispose(); } + ///搜索列表 + queryImSearch(keyword) async { + if (apiService == null) { + SharedPreferences value = await SharedPreferences.getInstance(); + apiService = ApiService( + Dio(), + context: context, + token: value.getString("token"), + showLoading:false + ); + } + BaseData> baseData = + await apiService.memberSearch(keyword).catchError((onError) {}); + if (baseData != null && baseData.isSuccess) { + searchUser.clear(); + searchUser.addAll(baseData.data); + searchState = 1; + + if(baseData.data.length == 0){ + searchState = 2; + } + setState(() {}); + } + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -52,15 +89,54 @@ class _ImSearch extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ imSearch(), - // Expanded( - // child: ListView.builder( - // itemCount: 10, - // physics: BouncingScrollPhysics(), - // shrinkWrap: true, - // itemBuilder: (context, position) { - // return imSearch(); - // }, - // )), + searchState == 2 ? + Center( + child: Text( + "未找到该用户", + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFFA29E9E), + ), + ), + ): + Expanded(child: + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if(searchState ==1) + Padding(padding:EdgeInsets.only(left:16.w), + child: Text( + "搜索用户:", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFF060606), + fontSize:16.sp, + fontWeight: MyFontWeight.medium, + ), + ),), + if(searchState ==1) + Expanded( + child: ListView.builder( + itemCount: searchUser?.length ?? 0, + physics: BouncingScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, position) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){ + setState(() { + searchUserIndex = position; + }); + Navigator.of(context).pushNamed('/router/personal_page', arguments: { + "memberId":searchUser[searchUserIndex].mid ?? "", + }); + }, + child: imSearchItem(searchUser[position]), + ); + }, + )) + ], + )), ], ), ), @@ -68,7 +144,7 @@ class _ImSearch extends State { ); } - ///搜索列表 + ///搜索 Widget imSearch() { return Container( margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,29.h), @@ -79,15 +155,39 @@ class _ImSearch extends State { ), child: TextField( textInputAction: TextInputAction.search, + onChanged: (value){ + setState(() { + searchState =3; + }); + if (isNumeric(value)) { + textType = 1; + } else { + textType = 2; + } + if(editingController.text == null || editingController.text == ""){ + return; + }else{ + queryImSearch(editingController.text ?? ""); + } + }, onEditingComplete: () { FocusScope.of(context).requestFocus(FocusNode()); + if(editingController.text == null || editingController.text == ""){ + SmartDialog.show( + widget: SettlementTips( + () {}, + text: "请输入姓名或手机号搜索", + )); + }else{ + queryImSearch(editingController.text ?? ""); + } }, controller: editingController, style: TextStyle( fontSize: 14.sp, ), decoration: InputDecoration( - hintText: "搜索", + hintText: "输入姓名或手机号搜索", hintStyle: TextStyle( fontSize: 14.sp, color: Color(0xFFA29E9E), @@ -108,4 +208,24 @@ class _ImSearch extends State { ), ); } + + ///搜索列表 + Widget imSearchItem(ImUser searchUser) { + return Container( + padding: EdgeInsets.only(left:10.w,right:16.w,bottom:15.h), + child: Text( + textType == 1 ?(searchUser?.phone ?? searchUser?.nickname?? "") : (searchUser?.nickname ?? searchUser?.phone ?? ""), + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF32A060), + fontWeight:MyFontWeight.regular), + ), + ); + } + + /// 判断给的字符串是否全部由数字组成 + bool isNumeric(String str) { + RegExp regExp = RegExp(r'^\d+$'); + return regExp.hasMatch(str); + } } diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index cb4b342f..06e3b291 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -176,7 +176,7 @@ class _FriendGroupList extends State { child: Row(children: [ Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(26.5.r), + borderRadius: BorderRadius.circular(100), ), child: MImage( list?.avatar ?? "", @@ -208,7 +208,7 @@ class _FriendGroupList extends State { behavior: HitTestBehavior.opaque, onTap:(){Navigator.of(context).pushNamed('/router/im_search');}, child: Container( - margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 24.h), padding: EdgeInsets.symmetric(vertical: 13.h), decoration: BoxDecoration( color: Color(0xFFFDFCFC), diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index fe435583..663179b7 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -1,25 +1,17 @@ -import 'dart:async'; -import 'dart:collection'; - import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/im_view/time_formatter.dart'; -import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; -import 'package:huixiang/retrofit/data/msg_stats.dart'; -import 'package:huixiang/retrofit/data/page.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_footer.dart'; import 'package:huixiang/view_widget/round_button.dart'; -import 'package:intl/intl.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; From 19b502e7eb8faa6c0f0aa680c8650379104ebf98 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 16:35:23 +0800 Subject: [PATCH 24/81] socketclient heartbeat --- lib/im/SocketClient.dart | 5 +++-- lib/im/database/hx_database.dart | 8 ++++---- lib/im/im_view/im_page.dart | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 4ec2a8e0..52dec77b 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -55,8 +55,9 @@ class SocketClient { heartbeat() { Timer.periodic(const Duration(milliseconds: 30000), (timer) { - debugPrint("heartbeat: ${DateTime.now().millisecondsSinceEpoch}"); - Uint8List data = utf8.encode(jsonEncode({"heartbeat": DateTime.now().millisecondsSinceEpoch})); + int milliseTime = DateTime.now().millisecondsSinceEpoch; + debugPrint("heartbeat: ${milliseTime}"); + Uint8List data = utf8.encode(jsonEncode({"heartbeat": milliseTime})); MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(3, 1, msgData.writeToBuffer()); _socket.add(proto2.toBytes()); diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 2af0d7a8..5637d8a1 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -89,10 +89,10 @@ class HxDatabase { if (db == null || !db.isOpen) { return Future.value({}); } - List messages = await db.query( - "Message", - where: 'fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0', - whereArgs: []..addAll(userIds)..add(selfUserId) + String userStr = userIds.join(","); + debugPrint("userStr: $userStr"); + List messages = await db.rawQuery( + "SELECT * FROM Message WHERE fromId IN ($userStr) AND toId = $selfUserId AND state = 0 AND isDelete = 0", ).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 663179b7..8f2cdee6 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -145,6 +145,7 @@ class _IMPage extends State implements OnChatMessage { /// update conversation unreadcount queryUnreadCount(userIds, userId) async { unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); + debugPrint("unreadCount: $unreadCountMap"); } /// update imuser info by mids From eee95c3d18892d4ae033a127b0f95548b25d7f1a Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 20 Sep 2024 16:53:12 +0800 Subject: [PATCH 25/81] =?UTF-8?q?=E9=9A=90=E8=97=8F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E9=A1=B5=E9=9D=A2;(=E8=AF=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=9A=82=E6=9C=AA=E5=BC=80=E5=8F=91);=20=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=A4=B4=E5=83=8F=E5=87=BA=E6=9D=A5=EF=BC=9B=20?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=A1=86=E6=BB=91=E5=8A=A8=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=EF=BC=9B=20AppUtils=E6=96=87=E4=BB=B6=E6=96=B0=E5=A2=9EtimeFor?= =?UTF-8?q?matter=EF=BC=88im=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=20=E6=B6=88=E6=81=AF=E9=A1=B5=E9=9D=A2=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=EF=BC=9B=20?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E8=B4=A6=E5=8F=B7=E6=B6=88=E6=81=AF=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=95=B0=E6=8D=AE=E5=BC=82=E5=B8=B8bug=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 2 +- lib/im/chat_details_page.dart | 98 +++++++++++++++++++++++++----- lib/im/chat_friend_group.dart | 52 ++++++++-------- lib/im/database/hx_database.dart | 86 ++++++++++++-------------- lib/im/im_search.dart | 10 +-- lib/im/im_view/im_page.dart | 4 +- lib/im/im_view/time_formatter.dart | 20 ------ lib/main.dart | 4 ++ lib/main_page.dart | 1 + lib/utils/flutter_utils.dart | 18 ++++++ 10 files changed, 181 insertions(+), 114 deletions(-) delete mode 100644 lib/im/im_view/time_formatter.dart diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 4ec2a8e0..023313cf 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -84,8 +84,8 @@ class SocketClient { dispose() { if (_socket != null) { - _socket = null; _socket.close(); + _socket = null; } } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 98eb5086..b0dee47f 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -1,13 +1,14 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:ui'; +import 'package:dio/dio.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/im/database/message.dart'; -import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; @@ -18,6 +19,9 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; +import '../retrofit/data/base_data.dart'; +import '../retrofit/data/user_info.dart'; +import '../utils/flutter_utils.dart'; import '../view_widget/custom_image.dart'; import 'im_view/on_chat_message.dart'; import 'im_view/on_chat_msg_instance.dart'; @@ -51,6 +55,7 @@ class _ChatDetailsPage extends State bool needHideMore = false; var commentFocus = FocusNode(); String hintText = "输入消息内容..."; + UserInfo userInfo; final OnChatMessage _tempOnChatMessage = OnChatMsgInstance.instance.onChatMessage; final ScrollController scrollController = ScrollController(); @@ -81,12 +86,29 @@ class _ChatDetailsPage extends State messages.insert(0, message); refreshState(); + // if (scrollController.position != null) { + // double offset = scrollController.position.maxScrollExtent; + // debugPrint("offset: $offset"); + // Future.delayed(const Duration(milliseconds: 500), () { + // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + // }); + // } if (scrollController.position != null) { - double offset = scrollController.position.maxScrollExtent; - debugPrint("offset: $offset"); - Future.delayed(const Duration(milliseconds: 500), () { - scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - }); + double maxScrollExtent = scrollController.position.maxScrollExtent; + double viewportDimensions = scrollController.position.viewportDimension; + + // 检查是否需要滚动 + if (maxScrollExtent > viewportDimensions) { + double offset = maxScrollExtent; + debugPrint("offset: $offset"); + Future.delayed(const Duration(milliseconds: 500), () { + scrollController.animateTo( + offset + 108, + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn + ); + }); + } } }); @@ -97,6 +119,28 @@ class _ChatDetailsPage extends State }); } + ///查询个人信息 + queryUser() async { + final SharedPreferences value = await SharedPreferences.getInstance(); + if (value.containsKey('user') && + value.getString('user') != null && + value.getString('user') != "") { + userInfo = UserInfo.fromJson(jsonDecode(value.getString('user'))); + } + if(apiService == null) + apiService = ApiService(Dio(), context: context, token: value.getString("token")); + BaseData baseData = + await apiService.queryInfo().catchError((onError) {}); + if (baseData != null && baseData.isSuccess) { + setState(() { + userInfo = baseData.data; + }); + SharedPreferences.getInstance().then((value) => { + value.setString('user', jsonEncode(baseData.data)), + }); + } + } + refreshState() { if (mounted) setState(() {}); } @@ -110,6 +154,7 @@ class _ChatDetailsPage extends State WidgetsBinding.instance.addObserver(this); commentFocus.addListener(_focusNodeListener); + queryUser(); loadMessageList(); } @@ -356,7 +401,7 @@ class _ChatDetailsPage extends State child: Column( children: [ Text( - "${DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))}", + AppUtils.timeFormatter(DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))), textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFA29E9E), @@ -691,7 +736,7 @@ class _ChatDetailsPage extends State width: 12.w, ), MImage( - _toUser.avatar, + userInfo?.headimg??"", isCircle: true, height: 44.h, width: 44.h, @@ -826,13 +871,38 @@ class _ChatDetailsPage extends State chatController.clear(); if (mounted) setState(() {}); - if (scrollController.position != null) { - double offset = scrollController.position.maxScrollExtent; - debugPrint("offset: $offset"); - Future.delayed(const Duration(milliseconds: 500), () { - scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - }); + // if (scrollController.position != null) { + // double offset = scrollController.position.maxScrollExtent; + // debugPrint("offset: $offset"); + // Future.delayed(const Duration(milliseconds: 500), () { + // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + // }); + // } + if (scrollController.position.maxScrollExtent > scrollController.offset) { + // 如果未滑动到底部,则自动滑动到底部 + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: Duration(seconds: 1), + curve: Curves.easeOut, + ); } + // if (scrollController.position != null) { + // double maxScrollExtent = scrollController.position.maxScrollExtent; + // double viewportDimensions = scrollController.position.viewportDimension; + // + // // 检查是否需要滚动 + // if (maxScrollExtent > viewportDimensions) { + // // double offset = maxScrollExtent; + // // debugPrint("offset: $offset"); + // Future.delayed(const Duration(milliseconds: 500), () { + // scrollController.animateTo( + // maxScrollExtent+viewportDimensions, + // duration: const Duration(milliseconds: 500), + // curve: Curves.easeIn + // ); + // }); + // } + // } }); }, diff --git a/lib/im/chat_friend_group.dart b/lib/im/chat_friend_group.dart index ccb49dc6..5e5657d5 100644 --- a/lib/im/chat_friend_group.dart +++ b/lib/im/chat_friend_group.dart @@ -84,32 +84,32 @@ class _ChatFriendGroup extends State leading: true, leadingColor: Colors.black, background: Color(0xFFFFFFFF), - action: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).pushNamed('/router/add_friend'); - }, - child: Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: Color(0xFFFFFFFF), - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: Color(0xFF000000).withAlpha(25), - offset: Offset(0, 0), - blurRadius: 4, - spreadRadius: 0, - ) - ], - ), - child: Image.asset( - "assets/image/add_friend.webp", - fit: BoxFit.fill, - height: 14.h,width: 14.h, - ), - ), - ), + // action: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: () { + // Navigator.of(context).pushNamed('/router/add_friend'); + // }, + // child: Container( + // padding: EdgeInsets.all(12), + // decoration: BoxDecoration( + // color: Color(0xFFFFFFFF), + // borderRadius: BorderRadius.circular(20.r), + // boxShadow: [ + // BoxShadow( + // color: Color(0xFF000000).withAlpha(25), + // offset: Offset(0, 0), + // blurRadius: 4, + // spreadRadius: 0, + // ) + // ], + // ), + // child: Image.asset( + // "assets/image/add_friend.webp", + // fit: BoxFit.fill, + // height: 14.h,width: 14.h, + // ), + // ), + // ), ), body: Container( child: Column( diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 2af0d7a8..7590f47f 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:huixiang/constant.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/migration.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; import '../../retrofit/data/im_user.dart'; @@ -37,10 +38,15 @@ class HxDatabase { db.close(); } - Future lastMessage(String userId) async { + _dbIsOpen() async { if (db == null || !db.isOpen) { - return Future.value(); + var sp = await SharedPreferences.getInstance(); + open(key: sp.getString("userId")); } + } + + Future lastMessage(String userId) async { + await _dbIsOpen(); String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; List messages = @@ -55,12 +61,11 @@ class HxDatabase { return (messages?.isNotEmpty ?? false) ? messages.first : null; } - Future> queryList(userId) { - if (db == null || !db.isOpen) { - return Future.value([]); - } + Future> queryList(userId) async{ + await _dbIsOpen(); String sql = 'SELECT * FROM (SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC) mm GROUP BY mm.toId,mm.fromId'; + debugPrint("66666666666$sql"); return db.rawQuery(sql, [userId, userId]).then((value) { return value.map((e) { debugPrint("Message: ${e}"); @@ -71,10 +76,8 @@ class HxDatabase { }); } - Future> queryUList(userId, {int page = 1, int pageSize = 10}) { - if (db == null || !db.isOpen) { - return Future.value([]); - } + Future> queryUList(userId, {int page = 1, int pageSize = 10}) async{ + await _dbIsOpen(); int start = (page - 1) * pageSize; String sql = 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT ?, ?'; @@ -85,26 +88,25 @@ class HxDatabase { }); } - Future> messageUnreadCount(List userIds, String selfUserId) async { - if (db == null || !db.isOpen) { - return Future.value({}); - } - List messages = await db.query( - "Message", - where: 'fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0', - whereArgs: []..addAll(userIds)..add(selfUserId) - ).then((value) { + Future> messageUnreadCount( + List userIds, String selfUserId) async { + await _dbIsOpen(); + List messages = await db + .query("Message", + where: 'fromId IN (?) AND toId = ? AND state = 0 AND isDelete = 0', + whereArgs: [] + ..addAll(userIds) + ..add(selfUserId)) + .then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Message-error: $error"); }); - return messages.lGroupBy((p)=> p.fromId).mGroupCount; + return (messages??[]).lGroupBy((p) => p.fromId).mGroupCount; } - Future> queryListAll() { - if (db == null || !db.isOpen) { - return Future.value(); - } + Future> queryListAll() async{ + await _dbIsOpen(); String sql = 'SELECT * FROM Message ORDER BY time DESC'; return db.rawQuery(sql); } @@ -113,35 +115,29 @@ class HxDatabase { return db.delete("Message"); } - update(Map message) { - if (db == null || !db.isOpen) { - return Future.value(0); - } + update(Map message) async{ + await _dbIsOpen(); debugPrint("Message_insert: $message"); return db.update("Message", message, where: 'id = ?', whereArgs: [message['id']]); } Future insert(Map message) async { - if (db == null || !db.isOpen) { - return Future.value(0); - } + await _dbIsOpen(); debugPrint("Message_insert: $message"); return db.insert("Message", message); } /// update message read state - readMessage(String selfUserId, String userId) { - if (db == null || !db.isOpen) { - return Future.value([]); - } - db.update("Message", {"state": 1}, where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", whereArgs: [userId, selfUserId]); + readMessage(String selfUserId, String userId) async{ + await _dbIsOpen(); + db.update("Message", {"state": 1}, + where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", + whereArgs: [userId, selfUserId]); } Future insertOrUpdateImUser(Map imUserMap) async { - if (db == null || !db.isOpen) { - return Future.value(0); - } + await _dbIsOpen(); debugPrint("imUser_insert: $imUserMap"); if ((await queryImUserById(imUserMap['mid'])) == null) return db.insert("ImUser", imUserMap); @@ -151,10 +147,9 @@ class HxDatabase { } Future> queryImUser(List userIds) async { - if (db == null || !db.isOpen) { - return Future.value([]); - } - String query = 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; + await _dbIsOpen(); + String query = + 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; return db.rawQuery(query).then((value) { return value.map((e) => ImUser.fromJson(e)).toList(); }, onError: (error) { @@ -163,9 +158,7 @@ class HxDatabase { } Future queryImUserById(String userId) async { - if (db == null || !db.isOpen) { - return Future.value(); - } + await _dbIsOpen(); List imUser = await db.query("ImUser", distinct: true, where: "mid = ?", whereArgs: [userId]).then((value) { return value.map((e) => ImUser.fromJson(e)).toList(); @@ -206,5 +199,4 @@ class HxDatabase { await migration.migrate(migrationDatabase); } } - } diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index 5768f20d..833df735 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -59,9 +58,12 @@ class _ImSearch extends State { await apiService.memberSearch(keyword).catchError((onError) {}); if (baseData != null && baseData.isSuccess) { searchUser.clear(); - searchUser.addAll(baseData.data); + baseData.data.forEach((element) { + if(element.phone != "" && element.nickname != ""){ + searchUser.add(element); + } + }); searchState = 1; - if(baseData.data.length == 0){ searchState = 2; } @@ -214,7 +216,7 @@ class _ImSearch extends State { return Container( padding: EdgeInsets.only(left:10.w,right:16.w,bottom:15.h), child: Text( - textType == 1 ?(searchUser?.phone ?? searchUser?.nickname?? "") : (searchUser?.nickname ?? searchUser?.phone ?? ""), + textType == 1 ?(searchUser?.phone?? "") : (searchUser?.nickname ?? ""), style: TextStyle( fontSize: 16.sp, color: Color(0xFF32A060), diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 663179b7..4428f370 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -4,7 +4,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/im/database/message.dart'; -import 'package:huixiang/im/im_view/time_formatter.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; @@ -182,6 +181,7 @@ class _IMPage extends State implements OnChatMessage { } refreshState() { + if(_refreshController.isRefresh)_refreshController.refreshCompleted(); if (mounted) setState(() {}); } @@ -488,7 +488,7 @@ class _IMPage extends State implements OnChatMessage { ), Text( lastMessageMap[userId]?.time != null - ? TimeFormatter.formatTime( + ? AppUtils.timeFormatter( DateTime.fromMillisecondsSinceEpoch(num.parse( lastMessageMap[userId]?.time ?? ""))) : "", diff --git a/lib/im/im_view/time_formatter.dart b/lib/im/im_view/time_formatter.dart deleted file mode 100644 index c406c9a5..00000000 --- a/lib/im/im_view/time_formatter.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:intl/intl.dart'; - -class TimeFormatter { - static String formatTime(DateTime time) { - final now = DateTime.now(); - final diff = now.difference(time).inHours; - - if (diff < 24) { - return '刚刚'; // 24小时内显示为“刚刚” - } else if (diff < 48) { - return '昨天'; // 昨天 - } else if (diff < 72) { - return '前天'; // 前天 - } else if (time.year == now.year) { - return DateFormat('MM月dd日').format(time); // 今年内的日期 - } else { - return DateFormat('yyyy年MM月dd日').format(time); // 其他年份的日期 - } - } -} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f3b09399..fe142f67 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -224,6 +224,10 @@ initDatabase(String userId) async { hxDatabase.open(key: userId); } +closeDatabase() async { + hxDatabase.close(); +} + final SocketClient socketClient = new SocketClient(); EventBus eventBus = EventBus(sync: true); diff --git a/lib/main_page.dart b/lib/main_page.dart index f2349dcf..7fb53437 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -70,6 +70,7 @@ class _MainPage extends State with WidgetsBindingObserver { void dispose() { super.dispose(); socketClient.dispose(); + closeDatabase(); WidgetsBinding.instance.removeObserver(this); } diff --git a/lib/utils/flutter_utils.dart b/lib/utils/flutter_utils.dart index a691abaa..e39eac84 100644 --- a/lib/utils/flutter_utils.dart +++ b/lib/utils/flutter_utils.dart @@ -288,4 +288,22 @@ class AppUtils { return true; } } + + ///im列表时间显示 + static String timeFormatter(DateTime time) { + final now = DateTime.now(); + final diff = now.difference(time).inHours; + + if (diff < 24) { + return '刚刚'; // 24小时内显示为“刚刚” + } else if (diff < 48) { + return '昨天'; // 昨天 + } else if (diff < 72) { + return '前天'; // 前天 + } else if (time.year == now.year) { + return DateFormat('MM月dd日').format(time); // 今年内的日期 + } else { + return DateFormat('yyyy年MM月dd日').format(time); // 其他年份的日期 + } + } } From d8bd13c4ebc17ff5cb67b69afc3b6bd092a60407 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 16:58:58 +0800 Subject: [PATCH 26/81] socketclient heartbeat --- lib/im/chat_details_page.dart | 41 +++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 98eb5086..d7e4a85d 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -11,9 +11,12 @@ import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; +import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; +import 'package:huixiang/view_widget/my_footer.dart'; import 'package:image_pickers/image_pickers.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; @@ -64,7 +67,9 @@ class _ChatDetailsPage extends State // SmartDialog.showToast("聊天 $txt", alignment: Alignment.center); } + RefreshController refreshController = RefreshController(); List messages = []; + int page = 0; loadMessageList() async { ImUser imUser = await hxDatabase.queryImUserById(_toUser.mid); @@ -75,7 +80,7 @@ class _ChatDetailsPage extends State selfUserId = (await SharedPreferences.getInstance()).getString("userId"); // unread msg 2 read state await hxDatabase.readMessage(selfUserId, _toUser.mid); - messages = await hxDatabase.queryUList(_toUser.mid, pageSize: 100); + await refresh(); socketClient.addCallback(_toUser.mid, (Message message) { messages.insert(0, message); @@ -97,6 +102,18 @@ class _ChatDetailsPage extends State }); } + refresh() async { + page += 1; + List messagePage = await hxDatabase.queryUList(_toUser.mid, page: page, pageSize: 10); + if (page == 1) { + messages = messagePage; + } else { + messages.addAll(messagePage); + } + refreshController.refreshCompleted(); + return; + } + refreshState() { if (mounted) setState(() {}); } @@ -292,7 +309,25 @@ class _ChatDetailsPage extends State child: Column( children: [ Expanded( - child: SingleChildScrollView( + child: SmartRefresher( + enablePullDown: true, + enablePullUp: false, + header: MyHeader(), + physics: BouncingScrollPhysics(), + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return (messages.length == 0) ? Container() : MyFooter(mode); + }, + ), + controller: refreshController, + onRefresh: () { + refresh().then((){ + refreshState(); + }); + }, + onLoading: () {}, + child: SingleChildScrollView( physics: BouncingScrollPhysics(), controller: scrollController, child: Column( @@ -313,6 +348,7 @@ class _ChatDetailsPage extends State ], ), ), + ), flex: 1, ), input() @@ -326,6 +362,7 @@ class _ChatDetailsPage extends State Widget chatDetailsList() { return Container( margin: EdgeInsets.only(bottom: 48.h), + alignment: Alignment.bottomCenter, child: ListView.builder( itemCount: messages.length, shrinkWrap: true, From 1a6e2967c6cc89e51dd61f9e422c18b9cd9550f2 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 20 Sep 2024 17:31:01 +0800 Subject: [PATCH 27/81] =?UTF-8?q?=E7=A9=BA=E5=AE=89=E5=85=A8=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 696c4d8f..157b6281 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -60,6 +60,7 @@ class SocketClient { Uint8List data = utf8.encode(jsonEncode({"heartbeat": milliseTime})); MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(3, 1, msgData.writeToBuffer()); + if(_socket != null) _socket.add(proto2.toBytes()); }); } From b0b2507f84bca59c307a92ef01112e47f8019da4 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 17:42:10 +0800 Subject: [PATCH 28/81] socketclient heartbeat --- lib/im/chat_details_page.dart | 169 +++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 75 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 048ea6fb..d9546db8 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -88,16 +88,9 @@ class _ChatDetailsPage extends State await refresh(); socketClient.addCallback(_toUser.mid, (Message message) { - messages.insert(0, message); + messages.insert(0, message..state = 1); refreshState(); - // if (scrollController.position != null) { - // double offset = scrollController.position.maxScrollExtent; - // debugPrint("offset: $offset"); - // Future.delayed(const Duration(milliseconds: 500), () { - // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - // }); - // } if (scrollController.position != null) { double maxScrollExtent = scrollController.position.maxScrollExtent; double viewportDimensions = scrollController.position.viewportDimension; @@ -107,15 +100,12 @@ class _ChatDetailsPage extends State double offset = maxScrollExtent; debugPrint("offset: $offset"); Future.delayed(const Duration(milliseconds: 500), () { - scrollController.animateTo( - offset + 108, + scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), - curve: Curves.easeIn - ); + curve: Curves.easeIn); }); } } - }); refreshState(); @@ -124,16 +114,21 @@ class _ChatDetailsPage extends State }); } - refresh() async { + Future refresh() async { + List messagePage = await hxDatabase.queryUList(_toUser.mid, page: page + 1, pageSize: 10); + if (messagePage.isEmpty) { + refreshController.loadNoData(); + return; + } else { + refreshController.loadComplete(); + } page += 1; - List messagePage = await hxDatabase.queryUList(_toUser.mid, page: page, pageSize: 10); if (page == 1) { messages = messagePage; } else { messages.addAll(messagePage); } - refreshController.refreshCompleted(); - return; + return Future.value(); } ///查询个人信息 @@ -144,17 +139,18 @@ class _ChatDetailsPage extends State value.getString('user') != "") { userInfo = UserInfo.fromJson(jsonDecode(value.getString('user'))); } - if(apiService == null) - apiService = ApiService(Dio(), context: context, token: value.getString("token")); + if (apiService == null) + apiService = + ApiService(Dio(), context: context, token: value.getString("token")); BaseData baseData = - await apiService.queryInfo().catchError((onError) {}); + await apiService.queryInfo().catchError((onError) {}); if (baseData != null && baseData.isSuccess) { setState(() { userInfo = baseData.data; }); SharedPreferences.getInstance().then((value) => { - value.setString('user', jsonEncode(baseData.data)), - }); + value.setString('user', jsonEncode(baseData.data)), + }); } } @@ -354,25 +350,46 @@ class _ChatDetailsPage extends State child: Column( children: [ Expanded( - child: SingleChildScrollView( + child: SmartRefresher( + enablePullDown: false, + enablePullUp: true, + header: MyHeader(), + reverse: true, physics: BouncingScrollPhysics(), - controller: scrollController, - child: Column( - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - setState(() { - emojiShowing = false; - isKeyBoardShow = emojiShowing; - moreShow = false; - isKeyBoardShow = moreShow; - FocusScope.of(context).requestFocus(FocusNode()); - }); - }, - child: chatDetailsList(), - ), - ], + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return (messages.length == 0) + ? Container() + : MyFooter(mode); + }, + ), + controller: refreshController, + onLoading: () { + refresh().then((value) { + refreshState(); + }); + }, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: scrollController, + child: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + setState(() { + emojiShowing = false; + isKeyBoardShow = emojiShowing; + moreShow = false; + isKeyBoardShow = moreShow; + FocusScope.of(context).requestFocus(FocusNode()); + }); + }, + child: chatDetailsList(), + ), + ], + ), ), ), flex: 1, @@ -418,7 +435,8 @@ class _ChatDetailsPage extends State child: Column( children: [ Text( - AppUtils.timeFormatter(DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))), + AppUtils.timeFormatter( + DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))), textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFA29E9E), @@ -554,40 +572,41 @@ class _ChatDetailsPage extends State child: Container( alignment: Alignment.centerLeft, child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), - padding: EdgeInsets.symmetric( - vertical: 8.h, - horizontal: 12.w, - ), - child: GestureDetector( - onLongPress: () { - setState(() { - copyIndex = 1; - }); - }, - child: Text( - tex = message.content, - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Color(0XFF0D0D0D), - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + child: GestureDetector( + onLongPress: () { + setState(() { + copyIndex = 1; + }); + }, + child: Text( + tex = message.content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Color(0XFF0D0D0D), + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), ), ), ), - ),), + ), ), ], ), @@ -753,7 +772,7 @@ class _ChatDetailsPage extends State width: 12.w, ), MImage( - userInfo?.headimg??"", + userInfo?.headimg ?? "", isCircle: true, height: 44.h, width: 44.h, @@ -895,7 +914,8 @@ class _ChatDetailsPage extends State // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); // }); // } - if (scrollController.position.maxScrollExtent > scrollController.offset) { + if (scrollController.position.maxScrollExtent > + scrollController.offset) { // 如果未滑动到底部,则自动滑动到底部 scrollController.animateTo( scrollController.position.maxScrollExtent, @@ -920,7 +940,6 @@ class _ChatDetailsPage extends State // }); // } // } - }); }, maxLines: 8, From 58b09b5368d2e20906be6ca1e5d877126c788814 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 20 Sep 2024 18:06:52 +0800 Subject: [PATCH 29/81] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=BB=91=E5=8A=A8?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 82 ++++---------------------------- lib/im/chat_setting.dart | 12 ++++- lib/im/database/hx_database.dart | 4 ++ 3 files changed, 24 insertions(+), 74 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 048ea6fb..ddf7aac5 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -90,38 +90,10 @@ class _ChatDetailsPage extends State socketClient.addCallback(_toUser.mid, (Message message) { messages.insert(0, message); refreshState(); - - // if (scrollController.position != null) { - // double offset = scrollController.position.maxScrollExtent; - // debugPrint("offset: $offset"); - // Future.delayed(const Duration(milliseconds: 500), () { - // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - // }); - // } - if (scrollController.position != null) { - double maxScrollExtent = scrollController.position.maxScrollExtent; - double viewportDimensions = scrollController.position.viewportDimension; - - // 检查是否需要滚动 - if (maxScrollExtent > viewportDimensions) { - double offset = maxScrollExtent; - debugPrint("offset: $offset"); - Future.delayed(const Duration(milliseconds: 500), () { - scrollController.animateTo( - offset + 108, - duration: const Duration(milliseconds: 500), - curve: Curves.easeIn - ); - }); - } - } - - }); - refreshState(); - - Future.delayed(const Duration(milliseconds: 1000), () { jumpToBottom(); }); + refreshState(); + jumpToBottom(); } refresh() async { @@ -176,7 +148,9 @@ class _ChatDetailsPage extends State } void jumpToBottom() { - scrollController.position.jumpTo(scrollController.position.maxScrollExtent); + Future.delayed(const Duration(milliseconds: 100), () { + scrollController.position.jumpTo(scrollController.position.maxScrollExtent); + }); } void didChangeMetrics() { @@ -336,7 +310,7 @@ class _ChatDetailsPage extends State behavior: HitTestBehavior.opaque, onTap: () { setState(() { - Navigator.of(context).pushNamed('/router/chat_setting'); + Navigator.of(context).pushNamed('/router/chat_setting',arguments: {"userId": _toUser.mid}); copyIndex = 0; }); }, @@ -887,40 +861,7 @@ class _ChatDetailsPage extends State messages.insert(0, message); chatController.clear(); if (mounted) setState(() {}); - - // if (scrollController.position != null) { - // double offset = scrollController.position.maxScrollExtent; - // debugPrint("offset: $offset"); - // Future.delayed(const Duration(milliseconds: 500), () { - // scrollController.animateTo(offset + 108, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - // }); - // } - if (scrollController.position.maxScrollExtent > scrollController.offset) { - // 如果未滑动到底部,则自动滑动到底部 - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: Duration(seconds: 1), - curve: Curves.easeOut, - ); - } - // if (scrollController.position != null) { - // double maxScrollExtent = scrollController.position.maxScrollExtent; - // double viewportDimensions = scrollController.position.viewportDimension; - // - // // 检查是否需要滚动 - // if (maxScrollExtent > viewportDimensions) { - // // double offset = maxScrollExtent; - // // debugPrint("offset: $offset"); - // Future.delayed(const Duration(milliseconds: 500), () { - // scrollController.animateTo( - // maxScrollExtent+viewportDimensions, - // duration: const Duration(milliseconds: 500), - // curve: Curves.easeIn - // ); - // }); - // } - // } - + jumpToBottom(); }); }, maxLines: 8, @@ -943,9 +884,7 @@ class _ChatDetailsPage extends State behavior: HitTestBehavior.opaque, onTap: () { _onSmileyTap(); - Future.delayed(Duration(milliseconds: 500), () { - jumpToBottom(); - }); + jumpToBottom(); }, child: Container( padding: EdgeInsets.only(left: 15.w, right: 8.w), @@ -959,10 +898,7 @@ class _ChatDetailsPage extends State behavior: HitTestBehavior.opaque, onTap: () { _onMoreTap(); - - Future.delayed(Duration(milliseconds: 500), () { - jumpToBottom(); - }); + jumpToBottom(); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), diff --git a/lib/im/chat_setting.dart b/lib/im/chat_setting.dart index ed32b6f6..1b175a05 100644 --- a/lib/im/chat_setting.dart +++ b/lib/im/chat_setting.dart @@ -4,13 +4,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; +import '../main.dart'; class ChatSetting extends StatefulWidget { + final Map arguments; + + ChatSetting({this.arguments}); @override State createState() { @@ -72,6 +77,7 @@ class _ChatSetting extends State{ ], ), SizedBox(height:31.h,), + GestureDetector(child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, @@ -91,7 +97,11 @@ class _ChatSetting extends State{ color: Colors.black, ), ], - ) + ),onTap: ()async{ + // await hxDatabase.deleteByUser(widget.arguments["userId"]??""); + // SmartDialog.showToast("删除成功", + // alignment: Alignment.center); + },) ], ), ), diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 2d277a41..ec2b78af 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -107,6 +107,10 @@ class HxDatabase { return db.rawQuery(sql); } + Future deleteByUser(String userId) async { + return db.delete("Message",where: "fromId = ? OR toId = ?", whereArgs: [userId, userId]); + } + Future deleteAll() async { return db.delete("Message"); } From acb709a38483fc05422793987ac12bae69cccc15 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 18:13:16 +0800 Subject: [PATCH 30/81] socketclient heartbeat --- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ lib/im/chat_details_page.dart | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 18f207d8..aa352e6d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -602,7 +602,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -653,7 +653,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.31; + MARKETING_VERSION = 3.2.32; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -801,7 +801,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -852,7 +852,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.31; + MARKETING_VERSION = 3.2.32; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -886,7 +886,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -937,7 +937,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.31; + MARKETING_VERSION = 3.2.32; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 284311f9..d1af9bb9 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -889,7 +889,7 @@ class _ChatDetailsPage extends State Message message = value; messages.insert(0, message); chatController.clear(); - if (mounted) setState(() {}); + refreshState(); jumpToBottom(); }); }, From c93335b3fc9021c3b4e4ba429eb00333ee5154e8 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Sep 2024 20:58:44 +0800 Subject: [PATCH 31/81] socketclient heartbeat --- .../community_view/community_dynamic.dart | 1 + lib/im/chat_details_page.dart | 145 +++++++++--------- lib/im/im_view/friend_groip_list.dart | 12 +- lib/im/im_view/im_page.dart | 4 + lib/main.dart | 6 + lib/mine/personal_page.dart | 9 ++ 6 files changed, 101 insertions(+), 76 deletions(-) diff --git a/lib/community/community_view/community_dynamic.dart b/lib/community/community_view/community_dynamic.dart index 8ed024db..d026201e 100644 --- a/lib/community/community_view/community_dynamic.dart +++ b/lib/community/community_view/community_dynamic.dart @@ -560,6 +560,7 @@ class _CommunityDynamic extends State { arguments: {"articleId": widget.article.id,"shareUrl":buildShareUrl()}); return; } else if (platform == ShareSDKPlatforms.facebook) { + Navigator.of(context).popAndPushNamed('/router/chat_details_page', arguments: {"articleId": widget.article.id, "shareUrl":buildShareUrl(),}); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index d1af9bb9..e8e2ab45 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -303,85 +303,82 @@ class _ChatDetailsPage extends State // setState(() {}); } } - return Scaffold( - // resizeToAvoidBottomInset: false, - backgroundColor: Color(0xFFF6F6F6), - appBar: MyAppBar( - title: _toUser.nickname, - titleColor: Color(0xFF0D0D0D), - titleSize: 17.sp, - leading: true, - leadingColor: Colors.black, - action: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - Navigator.of(context).pushNamed('/router/chat_setting',arguments: {"userId": _toUser.mid}); - copyIndex = 0; - }); - }, - child: Container( - alignment: Alignment.center, - child: Icon( - Icons.more_horiz, - color: Colors.black, - size: 30, + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + setState(() { + emojiShowing = false; + isKeyBoardShow = emojiShowing; + moreShow = false; + isKeyBoardShow = moreShow; + }); + }, + child: Scaffold( + // resizeToAvoidBottomInset: false, + backgroundColor: Color(0xFFF6F6F6), + appBar: MyAppBar( + title: _toUser.nickname, + titleColor: Color(0xFF0D0D0D), + titleSize: 17.sp, + leading: true, + leadingColor: Colors.black, + action: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + Navigator.of(context).pushNamed('/router/chat_setting',arguments: {"userId": _toUser.mid}); + copyIndex = 0; + }); + }, + child: Container( + alignment: Alignment.center, + child: Icon( + Icons.more_horiz, + color: Colors.black, + size: 30, + ), + ), ), ), - ), - ), - body: Container( - child: Column( - children: [ - Expanded( - child: SmartRefresher( - enablePullDown: false, - enablePullUp: true, - header: MyHeader(), - reverse: true, - physics: BouncingScrollPhysics(), - footer: CustomFooter( - loadStyle: LoadStyle.ShowWhenLoading, - builder: (BuildContext context, LoadStatus mode) { - return (messages.length == 0) - ? Container() - : MyFooter(mode); - }, - ), - controller: refreshController, - onLoading: () { - refresh().then((value) { - refreshState(); - }); - }, - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - child: Column( - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - setState(() { - emojiShowing = false; - isKeyBoardShow = emojiShowing; - moreShow = false; - isKeyBoardShow = moreShow; - FocusScope.of(context).requestFocus(FocusNode()); - }); - }, - child: chatDetailsList(), - ), - ], + body: Container( + child: Column( + children: [ + Expanded( + child: SmartRefresher( + enablePullDown: false, + enablePullUp: true, + header: MyHeader(), + reverse: true, + physics: BouncingScrollPhysics(), + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return (messages.length == 0) + ? Container() + : MyFooter(mode); + }, + ), + controller: refreshController, + onLoading: () { + refresh().then((value) { + refreshState(); + }); + }, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: scrollController, + child: chatDetailsList() + ), ), + flex: 1, ), - ), - flex: 1, + input() + ], ), - input() - ], - ), - ), + ), + ) ); } diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index 06e3b291..7d624d42 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -2,6 +2,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -151,20 +153,26 @@ class _FriendGroupList extends State { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (list[position].mid == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } Navigator.of(context).pushNamed( '/router/chat_details_page', arguments: { "toUser": ImUser( avatar: list[position].avatar, mid: list[position].mid, - nickname: list[position].nickname), + nickname: list[position].nickname, + ), }, ); }, child: friendGroupItem(list[position]), ); }, - )), + ), + ), ], ), ); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 4cf6e560..f7def394 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -431,6 +431,10 @@ class _IMPage extends State implements OnChatMessage { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (userIds[position] == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } Navigator.of(context).pushNamed( '/router/chat_details_page', arguments: { diff --git a/lib/main.dart b/lib/main.dart index fe142f67..a58e20d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -208,6 +208,7 @@ void main() async { ImgCachePath(); // initSdk(); bool isFirst = sharedPreferences.getBool("isFirst"); + initShared(); runApp(MyApp(locale, isFirst)); // FlutterBugly.postCatchedException((){ @@ -218,6 +219,11 @@ void main() async { // final XgFlutterPlugin xgFlutterPlugin = XgFlutterPlugin(); HxDatabase hxDatabase; +SharedPreferences sharedPreferences; + +initShared() async { + sharedPreferences = await SharedPreferences.getInstance(); +} initDatabase(String userId) async { hxDatabase = HxDatabase(); diff --git a/lib/mine/personal_page.dart b/lib/mine/personal_page.dart index 4ed4873d..09fdb547 100644 --- a/lib/mine/personal_page.dart +++ b/lib/mine/personal_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/community/community_view/community_dynamic.dart'; import 'package:huixiang/community/photo_view_gallery_screen.dart'; import 'package:huixiang/generated/l10n.dart'; +import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/article.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/data/comunity_comment.dart'; @@ -862,6 +863,10 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (memberInfor?.id == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } Navigator.of(context) .pushNamed('/router/chat_details_page',arguments:{ "toUser": ImUser( @@ -1298,6 +1303,10 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + if (memberInfor?.id == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } Navigator.of(context) .pushNamed('/router/chat_details_page', arguments: { "toUser": ImUser( From 03cb5df501e4244e157bd21d906bc78528ad64a4 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Mon, 23 Sep 2024 18:14:12 +0800 Subject: [PATCH 32/81] =?UTF-8?q?=E7=BD=AE=E9=A1=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=9B=20=E6=B8=85=E7=A9=BA=E8=AE=B0=E5=BD=95=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=9B=20=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=9B=20=E6=90=9C=E7=B4=A2=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community_view/community_dynamic.dart | 1 + lib/im/SocketClient.dart | 4 +- lib/im/chat_details_page.dart | 133 +++++++++++------- lib/im/chat_setting.dart | 92 +++++++----- lib/im/database/hx_database.dart | 4 + lib/im/im_search.dart | 40 +++++- lib/im/im_view/im_page.dart | 54 ++++--- lib/main.dart | 2 +- lib/mine/fans_page.dart | 1 + lib/mine/follow_page.dart | 1 + lib/mine/mine_page.dart | 1 + lib/mine/personal_page.dart | 20 +-- lib/retrofit/data/im_user.dart | 12 ++ lib/union/union_list.dart | 1 + lib/utils/flutter_utils.dart | 55 +++++++- lib/web/web_view/comment_list.dart | 1 + 16 files changed, 292 insertions(+), 130 deletions(-) diff --git a/lib/community/community_view/community_dynamic.dart b/lib/community/community_view/community_dynamic.dart index d026201e..b55d6a9c 100644 --- a/lib/community/community_view/community_dynamic.dart +++ b/lib/community/community_view/community_dynamic.dart @@ -174,6 +174,7 @@ class _CommunityDynamic extends State { "memberId": (widget.article.author == widget.userId) ? "0" : widget.article.author, + "inletType":0 }); widget.exitFull(); } diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 157b6281..de946e28 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -19,7 +19,8 @@ class SocketClient { connect() async { shared = await SharedPreferences.getInstance(); - await Socket.connect('192.168.10.129', 9090).then((value) { + //47.93.216.24:9090 线上 192.168.10.129:9090 测试 + await Socket.connect('47.93.216.24', 9090).then((value) { debugPrint("socket-connect"); _socket = value; _socket.listen((data) { @@ -38,6 +39,7 @@ class SocketClient { }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); + reconnect(); }, onDone: () { debugPrint("socket-listen-down: down"); }); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index e8e2ab45..5c1c4987 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -97,19 +97,20 @@ class _ChatDetailsPage extends State } Future refresh() async { - List messagePage = await hxDatabase.queryUList(_toUser.mid, page: page + 1, pageSize: 10); - if (messagePage.isEmpty) { - refreshController.loadNoData(); - return; - } else { - refreshController.loadComplete(); - } + List messagePage = + await hxDatabase.queryUList(_toUser.mid, page: page + 1, pageSize: 10); page += 1; if (page == 1) { messages = messagePage; } else { messages.addAll(messagePage); } + if (messagePage.isEmpty) { + refreshController.loadNoData(); + return; + } else { + refreshController.loadComplete(); + } return Future.value(); } @@ -155,7 +156,8 @@ class _ChatDetailsPage extends State void jumpToBottom() { Future.delayed(const Duration(milliseconds: 100), () { - scrollController.position.jumpTo(scrollController.position.maxScrollExtent); + scrollController.position + .jumpTo(scrollController.position.maxScrollExtent); }); } @@ -326,11 +328,16 @@ class _ChatDetailsPage extends State leadingColor: Colors.black, action: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - Navigator.of(context).pushNamed('/router/chat_setting',arguments: {"userId": _toUser.mid}); - copyIndex = 0; + onTap: () async { + await Navigator.of(context).pushNamed('/router/chat_setting', + arguments: {"userId": _toUser.mid}); + page = 0; + refresh().then((value) { + refreshState(); }); + // setState(() { + // copyIndex = 0; + // }); }, child: Container( alignment: Alignment.center, @@ -355,9 +362,10 @@ class _ChatDetailsPage extends State footer: CustomFooter( loadStyle: LoadStyle.ShowWhenLoading, builder: (BuildContext context, LoadStatus mode) { - return (messages.length == 0) - ? Container() - : MyFooter(mode); + // return (messages.length == 0) + // ? Container() + // : MyFooter(mode); + return SizedBox(); }, ), controller: refreshController, @@ -367,10 +375,9 @@ class _ChatDetailsPage extends State }); }, child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - child: chatDetailsList() - ), + physics: BouncingScrollPhysics(), + controller: scrollController, + child: chatDetailsList()), ), flex: 1, ), @@ -378,8 +385,7 @@ class _ChatDetailsPage extends State ], ), ), - ) - ); + )); } ///聊天列表 @@ -397,18 +403,19 @@ class _ChatDetailsPage extends State onTap: () { setState(() { copyIndex = 0; + FocusScope.of(context).requestFocus(FocusNode()); }); }, - child: chatDetailsItem(messages[position]), + child: chatDetailsItem(position), ); }, ), ); } - Widget chatDetailsItem(Message message) { - bool isSelf = message.fromId == selfUserId; - bool isText = message.msgType == 1; + Widget chatDetailsItem(position) { + bool isSelf = messages[position].fromId == selfUserId; + bool isText = messages[position].msgType == 1; return Container( padding: EdgeInsets.only( top: 32.h, @@ -416,8 +423,16 @@ class _ChatDetailsPage extends State child: Column( children: [ Text( - AppUtils.timeFormatter( - DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))), + // position == messages.length-1 + // ? + AppUtils.milliTimeFormatter(DateTime.fromMillisecondsSinceEpoch( + int.parse(messages[position].time))) + // : AppUtils.getTimeDisplay( + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position].time)), + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position+1].time))) + , textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFA29E9E), @@ -537,15 +552,24 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( children: [ - MImage( - _toUser.avatar, - isCircle: true, - height: 44.h, - width: 44.h, - fit: BoxFit.cover, - errorSrc: "assets/image/default_1.webp", - fadeSrc: "assets/image/default_1.webp", - ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed('/router/personal_page', + arguments: { + "memberId": _toUser?.mid ?? "", + "inletType": 1 + }); + }, + child: MImage( + _toUser.avatar, + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + )), SizedBox( width: 12.w, ), @@ -576,7 +600,7 @@ class _ChatDetailsPage extends State }); }, child: Text( - tex = message.content, + tex = messages[position].content, textAlign: TextAlign.left, style: TextStyle( height: 1.2.h, @@ -686,7 +710,7 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 36.w, right: 16.w), child: Row( children: [ - if (message.state == 3) + if (messages[position].state == 3) Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), @@ -705,7 +729,7 @@ class _ChatDetailsPage extends State ), ), ), - if (message.state == 3) + if (messages[position].state == 3) SizedBox( width: 12.w, ), @@ -736,7 +760,7 @@ class _ChatDetailsPage extends State }); }, child: Text( - tex = message.content, + tex = messages[position].content, textAlign: TextAlign.left, style: TextStyle( height: 1.2.h, @@ -752,20 +776,21 @@ class _ChatDetailsPage extends State SizedBox( width: 12.w, ), - MImage( - userInfo?.headimg ?? "", - isCircle: true, - height: 44.h, - width: 44.h, - fit: BoxFit.cover, - errorSrc: "assets/image/default_1.webp", - fadeSrc: "assets/image/default_1.webp", - ), - // Image.asset( - // "assets/image/fuka_zj.webp", - // height: 44, - // width: 44, - // ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed('/router/personal_page', + arguments: {"memberId": "0", "inletType": 1}); + }, + child: MImage( + userInfo?.headimg ?? "", + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + )), ], ), ), diff --git a/lib/im/chat_setting.dart b/lib/im/chat_setting.dart index 1b175a05..603efc46 100644 --- a/lib/im/chat_setting.dart +++ b/lib/im/chat_setting.dart @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../main.dart'; +import '../retrofit/data/im_user.dart'; class ChatSetting extends StatefulWidget { final Map arguments; @@ -23,13 +24,20 @@ class ChatSetting extends StatefulWidget { } } -class _ChatSetting extends State{ - ApiService apiService; - bool topSetting = false; +class _ChatSetting extends State { + ImUser imUser; @override void initState() { super.initState(); + initData(); + } + + void initData() async { + imUser = await hxDatabase.queryImUserById(widget.arguments["userId"]); + setState(() { + + }); } @override @@ -37,7 +45,7 @@ class _ChatSetting extends State{ return Scaffold( backgroundColor: Color(0xFFF9FAF7), appBar: MyAppBar( - title:"聊天设置", + title: "聊天设置", titleColor: Colors.black, titleSize: 18.sp, background: Colors.white, @@ -50,7 +58,7 @@ class _ChatSetting extends State{ Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 14.h), - padding: EdgeInsets.symmetric(horizontal: 16.w,vertical: 14.h), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h), child: Column( children: [ Row( @@ -61,52 +69,60 @@ class _ChatSetting extends State{ "置顶聊天", style: TextStyle( color: Color(0xFF353535), - fontSize:15.sp, + fontSize: 15.sp, fontWeight: MyFontWeight.semi_bold, ), ), CupertinoSwitch( - value: (topSetting), + value: ((imUser?.isTop ?? 0) == 1), activeColor: Color(0xFF32A060), - onChanged: (bool value) { - setState((){ - topSetting = !topSetting; - }); + onChanged: (bool value) async { + if (imUser == null) return; + imUser.isTop = value ? 1 : 0; + await hxDatabase + .insertOrUpdateImUser(imUser.toJson()); + setState(() {}); }, ), ], ), - SizedBox(height:31.h,), - GestureDetector(child: - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "清空聊天记录", - style: TextStyle( - color: Color(0xFF353535), - fontSize:15.sp, - fontWeight: MyFontWeight.semi_bold, + SizedBox( + height: 31.h, + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "清空聊天记录", + style: TextStyle( + color: Color(0xFF353535), + fontSize: 15.sp, + fontWeight: MyFontWeight.semi_bold, + ), ), - ), - Image.asset( - "assets/image/icon_right_z.webp", - height:16, - width:16, - color: Colors.black, - ), - ], - ),onTap: ()async{ - // await hxDatabase.deleteByUser(widget.arguments["userId"]??""); - // SmartDialog.showToast("删除成功", - // alignment: Alignment.center); - },) + Image.asset( + "assets/image/icon_right_z.webp", + height: 16, + width: 16, + color: Colors.black, + ), + ], + ), + onTap: () async { + await hxDatabase + .deleteByUser(widget.arguments["userId"] ?? ""); + SmartDialog.showToast("删除成功", + alignment: Alignment.center); + }, + ) ], ), ), - Expanded(child: - Container( + Expanded( + child: Container( color: Colors.white, )) ], diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index ec2b78af..812cb451 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -27,6 +27,10 @@ class HxDatabase { 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, PRIMARY KEY (`id`))'); }, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); + try { + await database.execute( + 'ALTER TABLE ImUser ADD COLUMN isTop INTEGER DEFAULT 0'); + }catch (e){} }, onUpgrade: (database, startVersion, endVersion) async { await runMigrations(database, startVersion, endVersion, _migrations); }, onOpen: (Database db) { diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index 833df735..d966c641 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../generated/l10n.dart'; import '../retrofit/data/base_data.dart'; import '../retrofit/data/im_user.dart'; import '../retrofit/retrofit_api.dart'; @@ -30,10 +31,14 @@ class _ImSearch extends State { int searchState = 0; int textType = 0; int searchUserIndex = 0; + String selfUserId = ""; @override void initState() { super.initState(); + SharedPreferences.getInstance().then((value) { + selfUserId = value.getString("userId"); + }); } ///离开页面记着销毁和清除 @@ -81,8 +86,7 @@ class _ImSearch extends State { backgroundColor: Color(0xFFFFFFFF), resizeToAvoidBottomInset: false, appBar: MyAppBar( - title: "", - leading: true, + title: "搜索", leadingColor: Colors.black, background: Color(0xFFFFFFFF), ), @@ -90,7 +94,33 @@ class _ImSearch extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - imSearch(), + Row( + crossAxisAlignment:CrossAxisAlignment.center, + mainAxisAlignment:MainAxisAlignment.center, + children: [ + Expanded(child: imSearch()), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap:(){ + Navigator.of(context).pop(); + }, + child: Container( + margin: EdgeInsets.only(top:8.h,bottom:29.h), + alignment: Alignment.center, + padding: EdgeInsets.only(right: 16.w), + child: Text( + S.of(context).quxiao, + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize:14.sp, + fontWeight: MyFontWeight.medium, + ), + ) + ), + ) + ], + ), searchState == 2 ? Center( child: Text( @@ -130,8 +160,10 @@ class _ImSearch extends State { searchUserIndex = position; }); Navigator.of(context).pushNamed('/router/personal_page', arguments: { - "memberId":searchUser[searchUserIndex].mid ?? "", + "memberId":(searchUser[searchUserIndex].mid ?? "") == selfUserId ? "0":searchUser[searchUserIndex].mid, + "inletType":0 }); + FocusScope.of(context).requestFocus(FocusNode()); }, child: imSearchItem(searchUser[position]), ); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index f7def394..617b8fbf 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; @@ -46,6 +47,8 @@ class _IMPage extends State implements OnChatMessage { Map lastMessageMap = {}; Map unreadCountMap = {}; Map contactMap = {}; + int slIndex = 0; + int insertIndex = 0; @override void onMessage(txt) { @@ -80,7 +83,7 @@ class _IMPage extends State implements OnChatMessage { if (userIds.contains(message.fromId)) { userIds.remove(message.fromId); } - userIds.insert(0, message.fromId); + userIds.insert(insertIndex, message.fromId); lastMessageMap[message.fromId] = message; @@ -161,6 +164,15 @@ class _IMPage extends State implements OnChatMessage { } } contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem(); + List topUserIds = [],notTopUserIds = []; + contactMap.forEach((key, value) { + if(value.isTop == 1) + topUserIds.add(key); + else + notTopUserIds.add(key); + }); + insertIndex = topUserIds.length; + this.userIds = topUserIds..addAll(notTopUserIds); } /// update one conversation last message ,and update conversation sort @@ -385,7 +397,9 @@ class _IMPage extends State implements OnChatMessage { return GestureDetector( behavior: HitTestBehavior.opaque, onTap:(){ - Navigator.of(context).pushNamed('/router/im_search'); + Navigator.of(context).pushNamed('/router/im_search').then((value) { + _refresh(); + }); }, child: Container( margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), @@ -429,24 +443,24 @@ class _IMPage extends State implements OnChatMessage { physics: NeverScrollableScrollPhysics(), itemBuilder: (context, position) { return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (userIds[position] == sharedPreferences.getString("userId")) { - SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); - return; - } - Navigator.of(context).pushNamed( - '/router/chat_details_page', - arguments: { - "toUser": contactMap[userIds[position]], - }, - ).then((value) { - unreadCountMap[userIds[position]] = 0; - updateLastMessage(userIds[position]); - }); - }, - child: chatItem(userIds[position]), - ); + behavior: HitTestBehavior.opaque, + onTap: () { + if (userIds[position] == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toUser": contactMap[userIds[position]], + }, + ).then((value) { + unreadCountMap[userIds[position]] = 0; + updateLastMessage(userIds[position]); + _refresh(); + }); + }, + child: chatItem(userIds[position])); }, ), ); diff --git a/lib/main.dart b/lib/main.dart index a58e20d9..bb72c565 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -517,7 +517,7 @@ Map routers = { '/router/chat_details_page': (context, {arguments}) => ChatDetailsPage(arguments: arguments), '/router/chat_setting': (context, {arguments}) => - ChatSetting(), + ChatSetting(arguments:arguments), '/router/chat_friend_group': (context, {arguments}) => ChatFriendGroup(), '/router/add_friend': (context, {arguments}) => diff --git a/lib/mine/fans_page.dart b/lib/mine/fans_page.dart index 27fd6a8d..1d2c4c18 100644 --- a/lib/mine/fans_page.dart +++ b/lib/mine/fans_page.dart @@ -154,6 +154,7 @@ class _FansPage extends State Navigator.of(context) .pushNamed('/router/personal_page', arguments: { "memberId": list.mid ?? "", + "inletType":0 });}, child: Padding( padding: EdgeInsets.only(right: 8.w), diff --git a/lib/mine/follow_page.dart b/lib/mine/follow_page.dart index 7608a692..b6a53b78 100644 --- a/lib/mine/follow_page.dart +++ b/lib/mine/follow_page.dart @@ -148,6 +148,7 @@ class _FollowPage extends State with SingleTickerProviderStateMixin, Navigator.of(context) .pushNamed('/router/personal_page', arguments: { "memberId": list.mid ?? "", + "inletType":0 });}, child: Padding( padding: EdgeInsets.only(right: 8.w), diff --git a/lib/mine/mine_page.dart b/lib/mine/mine_page.dart index 73c7e73b..c439664a 100644 --- a/lib/mine/mine_page.dart +++ b/lib/mine/mine_page.dart @@ -65,6 +65,7 @@ class MinePageState extends State with AutomaticKeepAliveClientMixin { : await Navigator.of(context) .pushNamed('/router/personal_page', arguments: { "memberId": "0", + "inletType":0 }); setState(() {}); } diff --git a/lib/mine/personal_page.dart b/lib/mine/personal_page.dart index 09fdb547..af905b6c 100644 --- a/lib/mine/personal_page.dart +++ b/lib/mine/personal_page.dart @@ -1307,14 +1307,18 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); return; } - Navigator.of(context) - .pushNamed('/router/chat_details_page', arguments: { - "toUser": ImUser( - avatar: memberInfor?.headimg, - mid: memberInfor?.id, - nickname: memberInfor?.nickname) - },); - + if(widget.arguments["inletType"] == 1){ + //聊天框跳转到该页面inletType = 1,别的页面都是0,为了避免打开多个聊天框bug; + Navigator.of(context).pop(); + }else{ + Navigator.of(context) + .pushNamed('/router/chat_details_page', arguments: { + "toUser": ImUser( + avatar: memberInfor?.headimg, + mid: memberInfor?.id, + nickname: memberInfor?.nickname) + },); + } }, child: Container( width:110.w, diff --git a/lib/retrofit/data/im_user.dart b/lib/retrofit/data/im_user.dart index a57953bb..80d15e33 100644 --- a/lib/retrofit/data/im_user.dart +++ b/lib/retrofit/data/im_user.dart @@ -10,11 +10,13 @@ class ImUser { String mid, String nickname, num isDelete, + num isTop, String avatar, String phone, }){ _mid = mid; _nickname = nickname; _isDelete = isDelete; + _isTop = isTop; _avatar = avatar; _phone = phone; } @@ -23,36 +25,46 @@ class ImUser { _mid = json['mid']; _nickname = json['nickname']; _isDelete = json['isDelete']; + _isTop = json['isTop']; _avatar = json['avatar']; _phone = json['phone']; } String _mid; String _nickname; num _isDelete; + num _isTop; String _avatar; String _phone; ImUser copyWith({ String mid, String nickname, num isDelete, + num isTop, String avatar, String phone, }) => ImUser( mid: mid ?? _mid, nickname: nickname ?? _nickname, isDelete: isDelete ?? _isDelete, + isTop: isTop ?? _isTop, avatar: avatar ?? _avatar, phone: phone ?? _phone, ); String get mid => _mid; String get nickname => _nickname; num get isDelete => _isDelete; + num get isTop => _isTop; String get avatar => _avatar; String get phone => _phone; + set isTop(num value) { + _isTop = value; + } + Map toJson() { final map = {}; map['mid'] = _mid; map['nickname'] = _nickname; map['isDelete'] = _isDelete; + map['isTop'] = _isTop; map['avatar'] = _avatar; map['phone'] = _phone; return map; diff --git a/lib/union/union_list.dart b/lib/union/union_list.dart index 2d70861b..4805f14c 100644 --- a/lib/union/union_list.dart +++ b/lib/union/union_list.dart @@ -129,6 +129,7 @@ class _UnionList extends State with AutomaticKeepAliveClientMixin { behavior: HitTestBehavior.opaque, onTap: () { { + //一心回乡商城版ui,因需求,暂未显示该板块(ui仿商城模式) // if (storeList[position].storeName == "一心回乡商城") { // Navigator.of(context).pushNamed( // '/router/shopping_mall_home', diff --git a/lib/utils/flutter_utils.dart b/lib/utils/flutter_utils.dart index e39eac84..6174d8b8 100644 --- a/lib/utils/flutter_utils.dart +++ b/lib/utils/flutter_utils.dart @@ -293,17 +293,64 @@ class AppUtils { static String timeFormatter(DateTime time) { final now = DateTime.now(); final diff = now.difference(time).inHours; + final diff2 = now.difference(time).inDays; if (diff < 24) { - return '刚刚'; // 24小时内显示为“刚刚” + return DateFormat('HH:mm').format(time); // 24小时内显示为今天发送信息的时间点 } else if (diff < 48) { - return '昨天'; // 昨天 - } else if (diff < 72) { - return '前天'; // 前天 + return '昨天 ${DateFormat('HH:mm').format(time)}'; // 昨天+时间点 + } else if (diff < 72 && diff2<7) { + return DateFormat('${DateFormat('EEEE').format(time)}').format(time);// 超过72小时显示为星期几 } else if (time.year == now.year) { return DateFormat('MM月dd日').format(time); // 今年内的日期 } else { return DateFormat('yyyy年MM月dd日').format(time); // 其他年份的日期 } } + + ///聊天框显示时间 + static String milliTimeFormatter(DateTime time) { + final now = DateTime.now(); + final diff = now.difference(time).inHours; + + if (diff < 24) { + return DateFormat('HH:mm').format(time); // 24小时内显示为今天发送信息的时间点 + } else if (diff < 48) { + return '昨天 ${DateFormat('HH:mm').format(time)}'; // 昨天+时间点 + } else if (diff < 72) { + return DateFormat('${DateFormat('EEEE').format(time)}').format(time) + " ${DateFormat('HH:mm').format(time)}";// 超过72小时显示为星期几+时间点 + } else if (time.year == now.year) { + return DateFormat('MM月dd日').format(time) + " ${DateFormat('HH:mm').format(time)}"; // 今年内的日期+时间点 + } else { + return DateFormat('yyyy年MM月dd日').format(time) + " ${DateFormat('HH:mm').format(time)}"; // 其他年份的日期+时间点 + } + } + + ///聊天框时间间隔显示 + static String getTimeDisplay(DateTime previousTime, DateTime currentTime) { + final diffInMinutes = (currentTime.difference(previousTime)).inMinutes; + + if (diffInMinutes < 5) { + // 时间间隔小于5分钟,不显示时间 + return ''; + } else if (currentTime.year == DateTime.now().year) { + // 同一年内 + if (currentTime.day == DateTime.now().day) { + // 当天 + return DateFormat('HH:mm').format(currentTime); + } else if (currentTime.day == DateTime.now().day - 1) { + // 昨天 + return '昨天 ${DateFormat('HH:mm').format(currentTime)}'; + } else if ((currentTime.day - DateTime.now().day).abs() < 7) { + // 最近七天 + return '${DateFormat('EEEE').format(currentTime)} ${DateFormat('HH:mm').format(currentTime)}'; + } else { + // 超过一周 + return DateFormat('MM/dd').format(currentTime); + } + } else { + // 不同年份 + return DateFormat('yyyy/MM/dd').format(currentTime); + } + } } diff --git a/lib/web/web_view/comment_list.dart b/lib/web/web_view/comment_list.dart index 6a5481a6..fc31663d 100644 --- a/lib/web/web_view/comment_list.dart +++ b/lib/web/web_view/comment_list.dart @@ -259,6 +259,7 @@ class CommentListState extends State { Navigator.of(context) .pushNamed('/router/personal_page', arguments: { "memberId": memberList.createUser, + "inletType":0 }); }); }, From 82a32633c387e1d395377ca6a369183034614334 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Mon, 23 Sep 2024 18:15:37 +0800 Subject: [PATCH 33/81] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_setting.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/im/chat_setting.dart b/lib/im/chat_setting.dart index 603efc46..75dbe0f9 100644 --- a/lib/im/chat_setting.dart +++ b/lib/im/chat_setting.dart @@ -114,8 +114,8 @@ class _ChatSetting extends State { onTap: () async { await hxDatabase .deleteByUser(widget.arguments["userId"] ?? ""); - SmartDialog.showToast("删除成功", - alignment: Alignment.center); + // SmartDialog.showToast("删除成功", + // alignment: Alignment.center); }, ) ], From d684ecf6258997f08f1834788df2762c1f9c1e2d Mon Sep 17 00:00:00 2001 From: zsw Date: Mon, 23 Sep 2024 20:32:39 +0800 Subject: [PATCH 34/81] socketclient --- lib/im/SocketClient.dart | 7 ++++++- lib/im/chat_details_page.dart | 2 +- lib/im/database/hx_database.dart | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index de946e28..8b739b0f 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -33,7 +33,12 @@ class SocketClient { Map messageMap = createMessage(userId, utf8.decode(dataResult.data), msgType: dataResult.type.value, userId: dataResult.from); Message message = Message.fromJson(messageMap); callbacks[userId]?.call(message); /// user self conversation callback - callbacks[dataResult.from]?.call(message); /// user conversation callback + + if (callbacks[dataResult.from] != null) { + messageMap["state"] = 1; + message.state = 1; + callbacks[dataResult.from].call(message); /// user conversation callback + } hxDatabase.insert(messageMap); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 5c1c4987..bc1a4ea2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -88,7 +88,7 @@ class _ChatDetailsPage extends State await refresh(); socketClient.addCallback(_toUser.mid, (Message message) { - messages.insert(0, message..state = 1); + messages.insert(0, message); refreshState(); jumpToBottom(); }); diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 812cb451..62a9a992 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -24,7 +24,7 @@ class HxDatabase { db.execute( 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); db.execute( - 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, PRIMARY KEY (`id`))'); + 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))'); }, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); try { From a0427102244f57f1faf5df6f6743f79ba4b50624 Mon Sep 17 00:00:00 2001 From: zsw Date: Tue, 24 Sep 2024 10:42:07 +0800 Subject: [PATCH 35/81] socketclient add debug setting --- lib/im/SocketClient.dart | 15 +++++++++------ lib/setting/about_page.dart | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 8b739b0f..fc774125 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -13,15 +13,17 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { + //47.93.216.24:9090 线上 192.168.10.129:9090 测试 + final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; + final num port = 9090; Socket _socket; SharedPreferences shared; connect() async { shared = await SharedPreferences.getInstance(); - //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - await Socket.connect('47.93.216.24', 9090).then((value) { - debugPrint("socket-connect"); + await Socket.connect(ip, port).then((value) { + debugPrint("socket-connect-$ip"); _socket = value; _socket.listen((data) { print(data); @@ -35,6 +37,7 @@ class SocketClient { callbacks[userId]?.call(message); /// user self conversation callback if (callbacks[dataResult.from] != null) { + // messageMap["state"] = 1; message.state = 1; callbacks[dataResult.from].call(message); /// user conversation callback @@ -72,7 +75,7 @@ class SocketClient { }); } - int reconnectTime = 1500; + int reconnectTime = 600; reconnect() { Future.delayed(Duration(milliseconds: reconnectTime *= 2), () { @@ -118,7 +121,7 @@ class SocketClient { }); if (!checkSocket()) { hxDatabase.update({"id": id, "state": 3}).catchError((error) { - debugPrint("insertMessage: $error"); + debugPrint("insertMessage: ${error.toString()}"); }); message["id"] = id; message["state"] = 3; @@ -135,7 +138,7 @@ class SocketClient { checkSocket() { if (_socket == null) { - reconnectTime = 1500; + reconnectTime = 1000; reconnect(); return false; } diff --git a/lib/setting/about_page.dart b/lib/setting/about_page.dart index 2c385a05..e3800c7c 100644 --- a/lib/setting/about_page.dart +++ b/lib/setting/about_page.dart @@ -1,10 +1,12 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:huixiang/main.dart'; import 'package:huixiang/utils/bridge.dart'; import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; @@ -46,7 +48,10 @@ class _AboutPage extends State { Container( decoration: new BoxDecoration( border: Border( - bottom: BorderSide(color: Color(0xffF7F7F7), width: 0.0)), + bottom: BorderSide( + color: Color(0xffF7F7F7), width: 0.0, + ), + ), color: Color(0xffF7F7F7), ), padding: EdgeInsets.only(top: 25.h), @@ -98,6 +103,15 @@ class _AboutPage extends State { }, child: textItem(S.of(context).shiyongbangzhu), ), + if (kDebugMode) + InkWell( + onTap: () { + Navigator.of(context).pushNamed( + '/router/help_feedback_page' + ); + }, + child: textItem("ip:${socketClient.ip} port:${socketClient.port} id:${socketClient.userId}"), + ), if (!Platform.isAndroid) GestureDetector( child: settingSingleItem(S.of(context).geiwopingfen), @@ -220,7 +234,8 @@ class _AboutPage extends State { size: 24, ), ], - )), + ), + ), ], ), ); From eba84a6e289f074a560ed2f05258b21fafcc21af Mon Sep 17 00:00:00 2001 From: zsw Date: Tue, 24 Sep 2024 10:44:36 +0800 Subject: [PATCH 36/81] socketclient add debug setting --- lib/setting/about_page.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/setting/about_page.dart b/lib/setting/about_page.dart index e3800c7c..36e38398 100644 --- a/lib/setting/about_page.dart +++ b/lib/setting/about_page.dart @@ -86,6 +86,8 @@ class _AboutPage extends State { SizedBox( height: 20.h, ), + if (kDebugMode) + textItem("ip: ${socketClient.ip}\nport: ${socketClient.port}\nid: ${socketClient.userId}"), GestureDetector( onTap: (){ // updateApp(); @@ -103,15 +105,6 @@ class _AboutPage extends State { }, child: textItem(S.of(context).shiyongbangzhu), ), - if (kDebugMode) - InkWell( - onTap: () { - Navigator.of(context).pushNamed( - '/router/help_feedback_page' - ); - }, - child: textItem("ip:${socketClient.ip} port:${socketClient.port} id:${socketClient.userId}"), - ), if (!Platform.isAndroid) GestureDetector( child: settingSingleItem(S.of(context).geiwopingfen), From 57b1e89ac3eff75596f43049e30e0c5e4b1c6d5c Mon Sep 17 00:00:00 2001 From: zsw Date: Tue, 24 Sep 2024 10:56:05 +0800 Subject: [PATCH 37/81] socketclient add debug setting --- lib/im/SocketClient.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index fc774125..0e1820ff 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -70,7 +70,10 @@ class SocketClient { Uint8List data = utf8.encode(jsonEncode({"heartbeat": milliseTime})); MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(3, 1, msgData.writeToBuffer()); - if(_socket != null) + if(!checkSocket()) { + timer.cancel(); + return; + } _socket.add(proto2.toBytes()); }); } From dff58c44fc03cb8d804dd8ab74582afe3714f6c0 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 11:34:40 +0800 Subject: [PATCH 38/81] =?UTF-8?q?im=E5=88=97=E8=A1=A8=E5=B7=A6=E6=BB=91?= =?UTF-8?q?=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 174 ++++++++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 27 deletions(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 617b8fbf..8fddb5d8 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -47,7 +47,6 @@ class _IMPage extends State implements OnChatMessage { Map lastMessageMap = {}; Map unreadCountMap = {}; Map contactMap = {}; - int slIndex = 0; int insertIndex = 0; @override @@ -436,32 +435,66 @@ class _IMPage extends State implements OnChatMessage { ///聊天列表 Widget chatList() { return Container( - child: ListView.builder( - padding: EdgeInsets.only(top: 16), - itemCount: userIds.length, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, position) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (userIds[position] == sharedPreferences.getString("userId")) { - SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); - return; - } - Navigator.of(context).pushNamed( - '/router/chat_details_page', - arguments: { - "toUser": contactMap[userIds[position]], - }, - ).then((value) { - unreadCountMap[userIds[position]] = 0; - updateLastMessage(userIds[position]); - _refresh(); - }); - }, - child: chatItem(userIds[position])); - }, + child: SlidableAutoCloseBehavior( + child: ListView.builder( + padding: EdgeInsets.only(top: 16), + itemCount: userIds.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, position) { + return ClipRRect( + // borderRadius: BorderRadius.all(Radius.circular(4)), + child: Slidable( + groupTag: true, + endActionPane: ActionPane( + extentRatio:0.16, + motion: ScrollMotion(), + children: [ + CustomSlidableAction( + onPressed: (bc) { + showDelDialog(userIds[position]); + }, + backgroundColor: Color(0xFFFB312B), + foregroundColor: Colors.white, + child: Container( + color: Colors.red, + height: double.infinity, + alignment: Alignment.center, + child: Text( + S.of(context).shanchu, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ), + ), + ], + ), + child:GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (userIds[position] == sharedPreferences.getString("userId")) { + SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); + return; + } + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toUser": contactMap[userIds[position]], + }, + ).then((value) { + unreadCountMap[userIds[position]] = 0; + updateLastMessage(userIds[position]); + _refresh(); + }); + }, + child: chatItem(userIds[position])), + ), + ); + }, + ), ), ); } @@ -562,4 +595,91 @@ class _IMPage extends State implements OnChatMessage { ), ); } + + + ///确认删除弹窗 + showDelDialog(userId) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, // 移除默认内边距 + content: Container( + // width: MediaQuery.of(context).size.width - 84, + height: 130.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child:Container( + alignment:Alignment.center, + child: Text( + "删除并清空聊天记录", + style: TextStyle( + color: Color(0xFFFF370A), + fontSize: 16.sp, + fontWeight: MyFontWeight.bold, + ), + ), + )), + // Spacer(), + Container( + height:1.h, + width: double.infinity, + color:Color(0xFFEDEDED), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: Text( + "取消", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF060606), + ) + ) + ) + ) + ), + Container( + height:45, + width: 1.w, + color: Color(0xFFEDEDED), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async{ + await hxDatabase.deleteByUser(contactMap[userId]?.mid ?? ""); + _refresh(); + Navigator.of(context).pop(); + }, + child: Text( + "确认", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFF060606), + ) + ) + ) + ) + ], + ) + ], + ), + ), + ); + }, + ); + } } From 1b097c28789964adfa837f65091ed1c487520cfa Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 11:50:35 +0800 Subject: [PATCH 39/81] =?UTF-8?q?im=E5=88=97=E8=A1=A8=E5=B7=A6=E6=BB=91?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=BC=B9=E7=AA=97=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 8fddb5d8..de0cbee1 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -619,7 +619,7 @@ class _IMPage extends State implements OnChatMessage { child: Text( "删除并清空聊天记录", style: TextStyle( - color: Color(0xFFFF370A), + color: Color(0xFF060606), fontSize: 16.sp, fontWeight: MyFontWeight.bold, ), @@ -668,7 +668,7 @@ class _IMPage extends State implements OnChatMessage { "确认", textAlign: TextAlign.center, style: TextStyle( - color: Color(0xFF060606), + color: Color(0xFFFF370A), ) ) ) From 82f9c2ba8695d94a44854123e9e1d8c6e548f4ca Mon Sep 17 00:00:00 2001 From: zsw Date: Tue, 24 Sep 2024 12:01:30 +0800 Subject: [PATCH 40/81] socketclient remove reconnect time --- lib/im/SocketClient.dart | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 0e1820ff..950eacdc 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -4,6 +4,8 @@ import 'dart:core'; import 'dart:core'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/im/Proto.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/out/auth.pb.dart'; @@ -17,11 +19,17 @@ class SocketClient { final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; final num port = 9090; Socket _socket; - SharedPreferences shared; + SharedPreferences _shared; connect() async { - shared = await SharedPreferences.getInstance(); + _shared = await SharedPreferences.getInstance(); + if (_socket != null) { + reconnect(); + return; + } + + showDebugToast("socket-connect .... "); await Socket.connect(ip, port).then((value) { debugPrint("socket-connect-$ip"); _socket = value; @@ -34,10 +42,9 @@ class SocketClient { Map messageMap = createMessage(userId, utf8.decode(dataResult.data), msgType: dataResult.type.value, userId: dataResult.from); Message message = Message.fromJson(messageMap); - callbacks[userId]?.call(message); /// user self conversation callback + callbacks[userId]?.call(message); /// user self conversation list callback if (callbacks[dataResult.from] != null) { - // messageMap["state"] = 1; message.state = 1; callbacks[dataResult.from].call(message); /// user conversation callback @@ -47,27 +54,35 @@ class SocketClient { }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); + showDebugToast("socket-listen-error: $error, stackTrace: $stackTrace"); reconnect(); }, onDone: () { debugPrint("socket-listen-down: down"); }); - authRequest(shared.getString("token")); + authRequest(_shared.getString("token")); heartbeat(); }).catchError((error) { debugPrint("socket-connect-error: $error"); + showDebugToast("socket-connect-error: $error"); reconnectTime = 1500; _socket = null; reconnect(); }); } + showDebugToast(text) { + if (kDebugMode) { + SmartDialog.showToast(text, alignment: Alignment.center); + } + } + heartbeat() { Timer.periodic(const Duration(milliseconds: 30000), (timer) { - int milliseTime = DateTime.now().millisecondsSinceEpoch; - debugPrint("heartbeat: ${milliseTime}"); - Uint8List data = utf8.encode(jsonEncode({"heartbeat": milliseTime})); + int millisecondsTime = DateTime.now().millisecondsSinceEpoch; + debugPrint("heartbeat: $millisecondsTime"); + Uint8List data = utf8.encode(jsonEncode({"heartbeat": millisecondsTime})); MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(3, 1, msgData.writeToBuffer()); if(!checkSocket()) { @@ -78,10 +93,10 @@ class SocketClient { }); } - int reconnectTime = 600; + int reconnectTime = 1500; reconnect() { - Future.delayed(Duration(milliseconds: reconnectTime *= 2), () { + Future.delayed(Duration(milliseconds: reconnectTime), () { dispose(); connect(); }); @@ -148,6 +163,6 @@ class SocketClient { return true; } - String get userId => shared.getString("userId"); + String get userId => _shared.getString("userId"); } \ No newline at end of file From 5166a60ddf67aba5c9afcd14424f549838291632 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 14:00:40 +0800 Subject: [PATCH 41/81] =?UTF-8?q?im=E6=90=9C=E7=B4=A2ui=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_search.dart | 327 +++++++++++++++++++++++++++--------------- 1 file changed, 214 insertions(+), 113 deletions(-) diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index d966c641..8cbd5794 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -12,10 +13,10 @@ import '../retrofit/data/base_data.dart'; import '../retrofit/data/im_user.dart'; import '../retrofit/retrofit_api.dart'; import '../utils/font_weight.dart'; +import '../view_widget/custom_image.dart'; import '../view_widget/my_appbar.dart'; import '../view_widget/settlement_tips_dialog.dart'; - class ImSearch extends StatefulWidget { @override State createState() { @@ -52,34 +53,33 @@ class _ImSearch extends State { queryImSearch(keyword) async { if (apiService == null) { SharedPreferences value = await SharedPreferences.getInstance(); - apiService = ApiService( - Dio(), + apiService = ApiService(Dio(), context: context, token: value.getString("token"), showLoading:false ); } BaseData> baseData = - await apiService.memberSearch(keyword).catchError((onError) {}); + await apiService.memberSearch(keyword).catchError((onError) {}); if (baseData != null && baseData.isSuccess) { - searchUser.clear(); - baseData.data.forEach((element) { - if(element.phone != "" && element.nickname != ""){ - searchUser.add(element); - } - }); - searchState = 1; - if(baseData.data.length == 0){ - searchState = 2; + searchUser.clear(); + baseData.data.forEach((element) { + if (element.phone != "" && element.nickname != "") { + searchUser.add(element); } - setState(() {}); + }); + searchState = 1; + if (baseData.data.length == 0) { + searchState = 2; + } + setState(() {}); } } @override Widget build(BuildContext context) { return GestureDetector( - onTap:(){ + onTap: () { FocusScope.of(context).requestFocus(FocusNode()); }, child: Scaffold( @@ -95,82 +95,109 @@ class _ImSearch extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment:CrossAxisAlignment.center, - mainAxisAlignment:MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded(child: imSearch()), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap:(){ - Navigator.of(context).pop(); - }, - child: Container( - margin: EdgeInsets.only(top:8.h,bottom:29.h), - alignment: Alignment.center, - padding: EdgeInsets.only(right: 16.w), - child: Text( - S.of(context).quxiao, - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize:14.sp, - fontWeight: MyFontWeight.medium, - ), - ) - ), - ) + Expanded(child: imSearch()), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + margin: EdgeInsets.only(top: 8.h, bottom: 29.h), + alignment: Alignment.center, + padding: EdgeInsets.only(right: 16.w), + child: Text( + S.of(context).quxiao, + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 14.sp, + fontWeight: MyFontWeight.medium, + ), + )), + ) ], ), - searchState == 2 ? - Center( - child: Text( - "未找到该用户", - style: TextStyle( - fontSize: 14.sp, - color: Color(0xFFA29E9E), - ), - ), - ): - Expanded(child: - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if(searchState ==1) - Padding(padding:EdgeInsets.only(left:16.w), + searchState == 2 + ? Center( child: Text( - "搜索用户:", - textAlign: TextAlign.center, + "未找到该用户", style: TextStyle( - color: Color(0xFF060606), - fontSize:16.sp, - fontWeight: MyFontWeight.medium, + fontSize: 14.sp, + color: Color(0xFFA29E9E), ), - ),), - if(searchState ==1) - Expanded( - child: ListView.builder( - itemCount: searchUser?.length ?? 0, - physics: BouncingScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, position) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap:(){ - setState(() { - searchUserIndex = position; - }); - Navigator.of(context).pushNamed('/router/personal_page', arguments: { - "memberId":(searchUser[searchUserIndex].mid ?? "") == selfUserId ? "0":searchUser[searchUserIndex].mid, - "inletType":0 - }); - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: imSearchItem(searchUser[position]), - ); - }, - )) - ], - )), + ), + ) + : Expanded( + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 19.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (editingController.text != "" && + searchState == 1) + Padding( + padding: EdgeInsets.only(left: 16.w), + child: Text( + "搜索用户:", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFF060606), + fontSize: 16.sp, + fontWeight: MyFontWeight.medium, + ), + ), + ), + if (editingController.text != "" && + searchState == 1) + Expanded( + child: Text( + editingController?.text ?? "", + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF32A060), + fontWeight: MyFontWeight.regular), + )) + ], + ), + ), + if (editingController.text != "" && searchState == 1) + Expanded( + child: ListView.builder( + itemCount: searchUser?.length ?? 0, + physics: BouncingScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, position) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + searchUserIndex = position; + }); + Navigator.of(context).pushNamed( + '/router/personal_page', + arguments: { + "memberId": (searchUser[searchUserIndex] + .mid ?? + "") == + selfUserId + ? "0" + : searchUser[searchUserIndex].mid, + "inletType": 0 + }); + FocusScope.of(context) + .requestFocus(FocusNode()); + }, + child: imSearchItem(searchUser[position]), + ); + }, + )) + ], + )), ], ), ), @@ -181,7 +208,7 @@ class _ImSearch extends State { ///搜索 Widget imSearch() { return Container( - margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,29.h), + margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 29.h), padding: EdgeInsets.symmetric(vertical: 13.h), decoration: BoxDecoration( color: Color(0xFFFDFCFC), @@ -189,30 +216,30 @@ class _ImSearch extends State { ), child: TextField( textInputAction: TextInputAction.search, - onChanged: (value){ + onChanged: (value) { setState(() { - searchState =3; + searchState = 3; }); - if (isNumeric(value)) { - textType = 1; - } else { - textType = 2; - } - if(editingController.text == null || editingController.text == ""){ - return; - }else{ - queryImSearch(editingController.text ?? ""); - } - }, + if (isNumeric(value)) { + textType = 1; + } else { + textType = 2; + } + if (editingController.text == null || editingController.text == "") { + return; + } else { + queryImSearch(editingController.text ?? ""); + } + }, onEditingComplete: () { FocusScope.of(context).requestFocus(FocusNode()); - if(editingController.text == null || editingController.text == ""){ + if (editingController.text == null || editingController.text == "") { SmartDialog.show( widget: SettlementTips( - () {}, - text: "请输入姓名或手机号搜索", - )); - }else{ + () {}, + text: "请输入姓名或手机号搜索", + )); + } else { queryImSearch(editingController.text ?? ""); } }, @@ -246,15 +273,89 @@ class _ImSearch extends State { ///搜索列表 Widget imSearchItem(ImUser searchUser) { return Container( - padding: EdgeInsets.only(left:10.w,right:16.w,bottom:15.h), - child: Text( - textType == 1 ?(searchUser?.phone?? "") : (searchUser?.nickname ?? ""), - style: TextStyle( - fontSize: 16.sp, - color: Color(0xFF32A060), - fontWeight:MyFontWeight.regular), - ), - ); + padding: EdgeInsets.only(left: 10.w, right: 16.w, bottom: 15.h), + child: Row( + children: [ + MImage( + searchUser?.avatar ?? "", + isCircle: true, + height: 54.h, + width: 54.h, + fit: BoxFit.cover, + errorSrc: "assets/image/fuka_zj.webp", + fadeSrc: "assets/image/fuka_zj.webp", + ), + SizedBox( + width: 12.w, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: TextStyle(color: Colors.black), // 默认文本颜色为黑色 + children: _splitText(searchUser?.nickname ?? "", + editingController.text) + .map((part) { + return TextSpan( + text: part, + style: part == editingController.text + ? TextStyle(color: Colors.green) // 匹配部分变为绿色 + : null, + ); + }).toList(), + ), + ), + SizedBox( + height: 7.h, + ), + RichText( + text: TextSpan( + style: TextStyle(color: Colors.black), // 默认文本颜色为黑色 + children: _splitText( + searchUser?.phone ?? "", editingController.text) + .map((part) { + return TextSpan( + text: part, + style: part == editingController.text + ? TextStyle(color: Colors.green) // 匹配部分变为绿色 + : null, + ); + }).toList(), + ), + ), + ], + ), + ), + ], + )); + } + + /// 搜索结构显示 + List _splitText(String text, String search) { + if (text == null || text.isEmpty || search == null || search.isEmpty) { + throw ArgumentError('text and search must not be null or empty'); + } + + final List parts = []; + int start = 0; + int index = text.indexOf(search); + + while (index != -1) { + if (index > start) { + parts.add(text.substring(start, index)); + } + parts.add(text.substring(index, index + search.length)); + start = index + search.length; + index = text.indexOf(search, start); + } + + if (start < text.length) { + parts.add(text.substring(start)); + } + + return parts; } /// 判断给的字符串是否全部由数字组成 From 281cdee4df0ac8b50d762c6941867c1348780c30 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 14:11:36 +0800 Subject: [PATCH 42/81] =?UTF-8?q?MImage=20=E9=BB=98=E8=AE=A4=E5=9B=BE?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=EF=BC=9B=20=E5=A5=BD=E5=8F=8B=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BB=96=E4=B8=BF=E5=A4=A7=E5=B0=8F=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 4 ++-- lib/im/contact_share.dart | 8 ++++---- lib/im/im_search.dart | 4 ++-- lib/im/im_view/friend_groip_list.dart | 10 +++++----- lib/im/im_view/im_page.dart | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index bc1a4ea2..6529616c 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -807,8 +807,8 @@ class _ChatDetailsPage extends State // width: 44, // height: 44, // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", + // errorSrc: "assets/image/default_1.webp", + // fadeSrc: "assets/image/default_1.webp", // ), Image.asset( "assets/image/fuka_zj.webp", diff --git a/lib/im/contact_share.dart b/lib/im/contact_share.dart index 7de99d3f..b144e1e4 100644 --- a/lib/im/contact_share.dart +++ b/lib/im/contact_share.dart @@ -160,8 +160,8 @@ class _ContactsShare extends State { // width: 44, // height: 44, // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", + // errorSrc: "assets/image/default_1.webp", + // fadeSrc: "assets/image/default_1.webp", // ), Image.asset( "assets/image/fuka_zj.webp", @@ -215,8 +215,8 @@ class _ContactsShare extends State { // width: 44, // height: 44, // fit: BoxFit.cover, - // errorSrc: "assets/image/default_user.webp", - // fadeSrc: "assets/image/default_user.webp", + // errorSrc: "assets/image/default_1.webp", + // fadeSrc: "assets/image/default_1.webp", // ), Image.asset( "assets/image/fuka_zj.webp", diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index 8cbd5794..7eeef2d1 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -282,8 +282,8 @@ class _ImSearch extends State { height: 54.h, width: 54.h, fit: BoxFit.cover, - errorSrc: "assets/image/fuka_zj.webp", - fadeSrc: "assets/image/fuka_zj.webp", + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, diff --git a/lib/im/im_view/friend_groip_list.dart b/lib/im/im_view/friend_groip_list.dart index 7d624d42..8a0c93f0 100644 --- a/lib/im/im_view/friend_groip_list.dart +++ b/lib/im/im_view/friend_groip_list.dart @@ -189,15 +189,15 @@ class _FriendGroupList extends State { child: MImage( list?.avatar ?? "", isCircle: true, - width: 66.h, - height: 66.h, + width: 54.h, + height: 54.h, fit: BoxFit.cover, - errorSrc: "assets/image/default_user.webp", - fadeSrc: "assets/image/default_user.webp", + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), ), Padding( - padding: EdgeInsets.only(left: 4.w), + padding: EdgeInsets.only(left:12.w), child: Text( list?.nickname ?? "", style: TextStyle( diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index de0cbee1..c1c48245 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -512,8 +512,8 @@ class _IMPage extends State implements OnChatMessage { height: 54.h, width: 54.h, fit: BoxFit.cover, - errorSrc: "assets/image/fuka_zj.webp", - fadeSrc: "assets/image/fuka_zj.webp", + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, From cfcad2400943b13d13d24912d2c8085f3e04c74a Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 15:31:50 +0800 Subject: [PATCH 43/81] =?UTF-8?q?=E5=A4=8D=E5=88=B6=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 441 ++++++++++++++++++---------------- 1 file changed, 232 insertions(+), 209 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 6529616c..1daff184 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -335,9 +335,6 @@ class _ChatDetailsPage extends State refresh().then((value) { refreshState(); }); - // setState(() { - // copyIndex = 0; - // }); }, child: Container( alignment: Alignment.center, @@ -457,90 +454,90 @@ class _ChatDetailsPage extends State // SizedBox( // height: 16.h, // ), - if (copyIndex == 1) - Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - padding: EdgeInsets.only(bottom: 13.h), - child: Container( - width: 180.w, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric( - horizontal: 32.w, - vertical: 7.5.h, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - copyIndex = 0; - this.copy(tex); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () {}, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ) - ], - ), - ), - ), - Image.asset( - "assets/image/icon_copy_j.webp", - height: 17, - width: 17, - ), - ], - ), + // if (copyIndex == -1) + // Stack( + // alignment: Alignment.bottomCenter, + // children: [ + // Container( + // padding: EdgeInsets.only(bottom: 13.h), + // child: Container( + // width: 180.w, + // decoration: BoxDecoration( + // color: Color(0xFF2A2A2A), + // borderRadius: BorderRadius.circular(6), + // ), + // padding: EdgeInsets.symmetric( + // horizontal: 32.w, + // vertical: 7.5.h, + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // GestureDetector( + // onTap: () { + // setState(() { + // copyIndex = 0; + // this.copy(tex); + // }); + // }, + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_copy.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // "复制", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ), + // GestureDetector( + // onTap: () {}, + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_delete.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // S.of(context).shanchu, + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ) + // ], + // ), + // ), + // ), + // Image.asset( + // "assets/image/icon_copy_j.webp", + // height: 17, + // width: 17, + // ), + // ], + // ), /// not self if (!isSelf && isText) @@ -617,88 +614,88 @@ class _ChatDetailsPage extends State ), ), - if (copyIndex == 1) - Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - padding: EdgeInsets.only(bottom: 13.h), - child: Container( - width: 180.w, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), - ), - padding: - EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - copyIndex = 0; - this.copy(tex); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () {}, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ) - ], - ), - ), - ), - Image.asset( - "assets/image/icon_copy_j.webp", - height: 17, - width: 17, - ), - ], - ), + // if (copyIndex == 1) + // Stack( + // alignment: Alignment.bottomCenter, + // children: [ + // Container( + // padding: EdgeInsets.only(bottom: 13.h), + // child: Container( + // width: 180.w, + // decoration: BoxDecoration( + // color: Color(0xFF2A2A2A), + // borderRadius: BorderRadius.circular(6), + // ), + // padding: + // EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // GestureDetector( + // onTap: () { + // setState(() { + // copyIndex = 0; + // this.copy(tex); + // }); + // }, + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_copy.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // "复制", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ), + // GestureDetector( + // onTap: () {}, + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_delete.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // S.of(context).shanchu, + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ) + // ], + // ), + // ), + // ), + // Image.asset( + // "assets/image/icon_copy_j.webp", + // height: 17, + // width: 17, + // ), + // ], + // ), /// self if (isSelf && isText) @@ -757,6 +754,7 @@ class _ChatDetailsPage extends State onLongPress: () { setState(() { copyIndex = 1; + // showCustomDialog(position); }); }, child: Text( @@ -1114,64 +1112,89 @@ class _ChatDetailsPage extends State ); } - showCustomDialog(BuildContext context, int position) { + ///复制弹窗 + showCustomDialog(int position) { showDialog( context: context, builder: (BuildContext context) { - return new AlertDialog( - backgroundColor: Color(0xFF2A2A2A), - elevation: 0, + return AlertDialog( + // backgroundColor: Color(0xFF2A2A2A), + // elevation: 0, contentPadding: EdgeInsets.only( top: 8.h, bottom: 5.h, ), content: Container( - height: 40.h, - // width:20.w, - alignment: Alignment.center, + width: 160.w, + height:50.h, + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(6), + ), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 7.5.h, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + GestureDetector( + onTap: () { + setState(() { + copyIndex = 0; + this.copy(tex); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, ), - ), - ], + SizedBox( + height: 2.h, + ), + Text( + "复制", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), ), - Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + GestureDetector( + onTap: () {}, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, ), - ), - ], + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), ) ], ), - ), + ) ); }); } From cbb84aa75d4835d1518e6c0fc43b253e763df882 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Tue, 24 Sep 2024 16:34:05 +0800 Subject: [PATCH 44/81] =?UTF-8?q?=E6=9C=80=E5=90=8E=E4=B8=80=E6=9D=A1?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=A1=BA=E5=BA=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/database/hx_database.dart | 23 +++++++++++++++++++---- lib/im/im_view/im_page.dart | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 62a9a992..0a8ce7d7 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -65,11 +65,26 @@ class HxDatabase { return (messages?.isNotEmpty ?? false) ? messages.first : null; } - Future> queryList(userId) async{ + Future> queryList() async{ await _dbIsOpen(); - String sql = - 'SELECT * FROM (SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC) mm GROUP BY mm.toId,mm.fromId'; - return db.rawQuery(sql, [userId, userId]).then((value) { + String sql = '''SELECT * + FROM `Message` + WHERE ROWID IN ( + SELECT ROWID + FROM ( + SELECT ROWID, `fromId`, `toId`, MAX(`time`) AS max_time + FROM `Message` + GROUP BY `fromId` || '_' || `toId`, `toId` || '_' || `fromId` + ) AS grouped_messages + WHERE max_time = ( + SELECT MAX(`time`) + FROM `Message` + WHERE (`fromId` = grouped_messages.`fromId` AND `toId` = grouped_messages.`toId`) + OR (`fromId` = grouped_messages.`toId` AND `toId` = grouped_messages.`fromId`) + ) + ) + ORDER BY `time` DESC;'''; + return db.rawQuery(sql).then((value) { return value.map((e) { debugPrint("Message: ${e}"); return Message.fromJson(e); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index c1c48245..bf44da4d 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -117,7 +117,7 @@ class _IMPage extends State implements OnChatMessage { loadMessageList() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); - messages = await hxDatabase.queryList(userId); + messages = await hxDatabase.queryList(); lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); From 0fb67d8cf00406df2ea87e42037070f89da617ec Mon Sep 17 00:00:00 2001 From: zsw Date: Tue, 24 Sep 2024 16:44:08 +0800 Subject: [PATCH 45/81] socketclient remove reconnect time --- lib/im/SocketClient.dart | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 950eacdc..4b45ec73 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -66,10 +66,12 @@ class SocketClient { }).catchError((error) { debugPrint("socket-connect-error: $error"); showDebugToast("socket-connect-error: $error"); - reconnectTime = 1500; _socket = null; reconnect(); }); + + // checkConnect(); + } showDebugToast(text) { @@ -89,17 +91,19 @@ class SocketClient { timer.cancel(); return; } - _socket.add(proto2.toBytes()); + try { + _socket.add(proto2.toBytes()); + } catch (e) { + debugPrint("socket-send-error: ${e.toString()}"); + showDebugToast("socket-send-error: ${e.toString()}"); + reconnect(); + } }); } - int reconnectTime = 1500; - reconnect() { - Future.delayed(Duration(milliseconds: reconnectTime), () { - dispose(); - connect(); - }); + dispose(); + connect(); } Map callbacks = {}; @@ -149,14 +153,22 @@ class SocketClient { Uint8List data = utf8.encode(content); MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.TEXT, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); - _socket.add(proto2.toBytes()); + try { + _socket.add(proto2.toBytes()); + } catch (e) { + hxDatabase.update({"id": id, "state": 3}).catchError((error) { + debugPrint("insertMessage: ${error.toString()}"); + }); + debugPrint("socket-send-error: ${e.toString()}"); + showDebugToast("socket-send-error: ${e.toString()}"); + reconnect(); + } debugPrint("sendMessage: ${message["id"]}"); return Message.fromJson(message); } checkSocket() { if (_socket == null) { - reconnectTime = 1000; reconnect(); return false; } From db76f9e15f66a01a9b1789c2e086e48679833c05 Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 25 Sep 2024 11:14:56 +0800 Subject: [PATCH 46/81] socketclient remove reconnect time --- assets/svg/shequn.svg | 11 +++-- lib/home/home_page.dart | 1 + lib/im/SocketClient.dart | 73 +++++++++++++++++++++----------- lib/im/chat_details_page.dart | 62 ++++++++++++++++----------- lib/im/database/hx_database.dart | 10 +++++ lib/im/database/message.dart | 2 + lib/im/im_view/im_page.dart | 72 ++----------------------------- lib/main_page.dart | 1 + 8 files changed, 109 insertions(+), 123 deletions(-) diff --git a/assets/svg/shequn.svg b/assets/svg/shequn.svg index e594bfec..49f53523 100644 --- a/assets/svg/shequn.svg +++ b/assets/svg/shequn.svg @@ -1,14 +1,13 @@ - + Created with Pixso. - - + + - - - + + diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 6d0b0e76..f6ecb7ac 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -855,4 +855,5 @@ class HomePageState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + } diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 4b45ec73..0f169bf1 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -20,6 +20,8 @@ class SocketClient { final num port = 9090; Socket _socket; SharedPreferences _shared; + Timer timer; + bool get heartbeatActive => timer != null && timer.isActive; connect() async { _shared = await SharedPreferences.getInstance(); @@ -41,17 +43,17 @@ class SocketClient { print('收到来自:${dataResult.from},消息内容: ${utf8.decode(dataResult.data)} '); Map messageMap = createMessage(userId, utf8.decode(dataResult.data), msgType: dataResult.type.value, userId: dataResult.from); - Message message = Message.fromJson(messageMap); - callbacks[userId]?.call(message); /// user self conversation list callback - if (callbacks[dataResult.from] != null) { messageMap["state"] = 1; - message.state = 1; - callbacks[dataResult.from].call(message); /// user conversation callback } - - hxDatabase.insert(messageMap); - + hxDatabase.insert(messageMap).then((value) { + messageMap["id"] = value; + Message message = Message.fromJson(messageMap); + if (callbacks[dataResult.from] != null) { + callbacks[dataResult.from].call(message); /// user conversation callback + } + callbacks[userId]?.call(message); /// user self conversation list callback + }); }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); showDebugToast("socket-listen-error: $error, stackTrace: $stackTrace"); @@ -69,9 +71,6 @@ class SocketClient { _socket = null; reconnect(); }); - - // checkConnect(); - } showDebugToast(text) { @@ -80,27 +79,53 @@ class SocketClient { } } + Proto heartbeatData() { + DateTime dateTime = DateTime.now(); + int millisecondsTime = dateTime.millisecondsSinceEpoch; + Uint8List data = utf8.encode(jsonEncode({"heartbeat": millisecondsTime})); + MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); + final proto2 = Proto(3, 1, msgData.writeToBuffer()); + debugPrint("heartbeat: ${dateTime.toString()}"); + return proto2; + } + heartbeat() { - Timer.periodic(const Duration(milliseconds: 30000), (timer) { - int millisecondsTime = DateTime.now().millisecondsSinceEpoch; - debugPrint("heartbeat: $millisecondsTime"); - Uint8List data = utf8.encode(jsonEncode({"heartbeat": millisecondsTime})); - MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data); - final proto2 = Proto(3, 1, msgData.writeToBuffer()); + cancelTimer(); + timer = Timer.periodic(const Duration(milliseconds: 30000), (timer) { if(!checkSocket()) { timer.cancel(); return; } - try { - _socket.add(proto2.toBytes()); - } catch (e) { - debugPrint("socket-send-error: ${e.toString()}"); - showDebugToast("socket-send-error: ${e.toString()}"); - reconnect(); - } + sendHeartBeatAndCheckSocket(); }); } + /// send heartBeat and check socket is connected + /// send error: reconnect, + /// else check Timer.periodic isActive , + /// if not active: Timer.periodic send heartBeat + sendHeartBeatAndCheckSocket() { + final proto2 = heartbeatData(); + try { + _socket.add(proto2.toBytes()); + if (!socketClient.heartbeatActive) { + heartbeat(); + debugPrint("socket-periodic-send-heart-beat"); + } + } catch (e) { + debugPrint("socket-send-error: ${e.toString()}"); + showDebugToast("socket-send-error: ${e.toString()}"); + reconnect(); + } + } + + cancelTimer() { + if (timer != null && timer.isActive) { + timer.cancel(); + timer = null; + } + } + reconnect() { dispose(); connect(); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 1daff184..4b982f75 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -89,8 +89,10 @@ class _ChatDetailsPage extends State socketClient.addCallback(_toUser.mid, (Message message) { messages.insert(0, message); - refreshState(); - jumpToBottom(); + messageShowTime().then((value) { + refreshState(); + jumpToBottom(); + }); }); refreshState(); jumpToBottom(); @@ -105,6 +107,7 @@ class _ChatDetailsPage extends State } else { messages.addAll(messagePage); } + await messageShowTime(); if (messagePage.isEmpty) { refreshController.loadNoData(); return; @@ -114,6 +117,14 @@ class _ChatDetailsPage extends State return Future.value(); } + Future messageShowTime() async { + List messagePages = await hxDatabase.queryTList(_toUser.mid); + for (var value in messages) { + Message message = messagePages.firstWhere((element) => value.id == element.id, orElse: () => null); + value.showTime = message != null; + } + } + ///查询个人信息 queryUser() async { final SharedPreferences value = await SharedPreferences.getInstance(); @@ -123,10 +134,8 @@ class _ChatDetailsPage extends State userInfo = UserInfo.fromJson(jsonDecode(value.getString('user'))); } if (apiService == null) - apiService = - ApiService(Dio(), context: context, token: value.getString("token")); - BaseData baseData = - await apiService.queryInfo().catchError((onError) {}); + apiService = ApiService(Dio(), context: context, token: value.getString("token")); + BaseData baseData = await apiService.queryInfo().catchError((onError) {}); if (baseData != null && baseData.isSuccess) { setState(() { userInfo = baseData.data; @@ -374,7 +383,8 @@ class _ChatDetailsPage extends State child: SingleChildScrollView( physics: BouncingScrollPhysics(), controller: scrollController, - child: chatDetailsList()), + child: chatDetailsList(), + ), ), flex: 1, ), @@ -382,7 +392,8 @@ class _ChatDetailsPage extends State ], ), ), - )); + ), + ); } ///聊天列表 @@ -419,24 +430,25 @@ class _ChatDetailsPage extends State ), child: Column( children: [ - Text( - // position == messages.length-1 - // ? - AppUtils.milliTimeFormatter(DateTime.fromMillisecondsSinceEpoch( - int.parse(messages[position].time))) - // : AppUtils.getTimeDisplay( - // DateTime.fromMillisecondsSinceEpoch( - // int.parse(messages[position].time)), - // DateTime.fromMillisecondsSinceEpoch( - // int.parse(messages[position+1].time))) - , - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, + if (messages[position].showTime) + Text( + // position == messages.length-1 + // ? + AppUtils.milliTimeFormatter(DateTime.fromMillisecondsSinceEpoch( + int.parse(messages[position].time))) + // : AppUtils.getTimeDisplay( + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position].time)), + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position+1].time))) + , + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFA29E9E), + fontSize: 10.sp, + fontWeight: MyFontWeight.regular, + ), ), - ), // if (messages.indexOf(message) == (messages.length - 1)) // Padding( // padding: EdgeInsets.only(top: 10.h, bottom: 24.h), diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 0a8ce7d7..7e307f57 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -106,6 +106,16 @@ class HxDatabase { }); } + Future> queryTList(userId) async{ + await _dbIsOpen(); + String sql = 'SELECT *, time / 300000 * 300000 AS time_interval FROM Message WHERE toId = ? OR fromId = ? GROUP BY time_interval ORDER BY time DESC'; + return db.rawQuery(sql, [userId, userId]).then((value) { + return value.map((e) => Message.fromJson(e)).toList(); + }, onError: (error) { + debugPrint("Messageerror: $error"); + }); + } + Future> messageUnreadCount(List userIds, String selfUserId) async { await _dbIsOpen(); String userStr = userIds.join(","); diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 188003b4..c5eadba2 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -21,6 +21,8 @@ class Message { int isDelete; + bool showTime = false; + Message(this.id, this.fromId, this.toId, this.replyId, this.content, this.attach, this.msgType, this.time, this.state, this.isDelete); factory Message.fromJson(Map json) => Message( diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index bf44da4d..724d5ce7 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -197,72 +197,6 @@ class _IMPage extends State implements OnChatMessage { if (mounted) setState(() {}); } - // queryMessage() async { - // BaseData> baseData = await apiService.msgList({ - // "pageNum": pageNum, - // "pageSize": 10, - // "searchKey": "", - // "state": "", - // "typed": "" - // }).catchError((onError) { - // _refreshController.loadFailed(); - // _refreshController.refreshFailed(); - // }); - // - // if (baseData != null && baseData.isSuccess) { - // if (pageNum == 1) { - // messages.clear(); - // } - // List message = []; - // message.addAll(baseData.data.list); - // message.forEach((element) { - // if (element.typed == 2 || element.typed == 3) { - // messages.add(element); - // } - // }); - // _refreshController.loadComplete(); - // _refreshController.refreshCompleted(); - // if (mounted) setState(() {}); - // if (pageNum * 10 > int.tryParse(baseData.data.total)) { - // _refreshController.loadNoData(); - // } else { - // pageNum += 1; - // } - // } else { - // _refreshController.loadFailed(); - // _refreshController.refreshFailed(); - // } - // } - - // queryMsgStats() async { - // if (apiService == null) { - // SharedPreferences value = await SharedPreferences.getInstance(); - // apiService = ApiService( - // Dio(), - // context: context, - // token: value.getString("token"), - // ); - // } - // BaseData> baseData = await apiService.stats().catchError((onError) { - // debugPrint("stats.error: $onError"); - // }); - // if (baseData != null && baseData.isSuccess) { - // setState(() { - // msgNumber.forEach((key, value) { - // msgNumber[key] = 0; - // }); - // baseData.data.forEach((element) { - // if (msgNumber.containsKey(element.name)) { - // msgNumber[element.name] = element.number; - // } - // }); - // }); - // _refreshController.loadComplete(); - // _refreshController.refreshCompleted(); - // } - // EasyLoading.dismiss(); - // } - ///批量查询用户信息 queryMemberInfo(List mids) async { if (mids.isEmpty) { @@ -271,8 +205,10 @@ class _IMPage extends State implements OnChatMessage { BaseData> baseData = await apiService.memberInfoByIds({ "mids": mids, }).catchError((error) { - SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), - alignment: Alignment.center); + SmartDialog.showToast( + AppUtils.dioErrorTypeToString(error.type), + alignment: Alignment.center, + ); }); if (baseData != null && baseData.isSuccess) { if (baseData.data.isNotEmpty) { diff --git a/lib/main_page.dart b/lib/main_page.dart index 7fb53437..f318ce9f 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -82,6 +82,7 @@ class _MainPage extends State with WidgetsBindingObserver { break; case AppLifecycleState.resumed: //从后台切换前台,界面可见 pushRoute(); + socketClient.sendHeartBeatAndCheckSocket(); if (DateTime.now().millisecondsSinceEpoch - lastTime > 420000) //处于后台**分钟后刷新应用 // Navigator.of(context).popAndPushNamed('/router/start_page'); From 2db144584fdf2bfa1011737184491b42d0ca1c2d Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 25 Sep 2024 11:21:06 +0800 Subject: [PATCH 47/81] socketclient remove reconnect time --- lib/im/chat_details_page.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 4b982f75..4b41a21a 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -132,6 +132,10 @@ class _ChatDetailsPage extends State value.getString('user') != null && value.getString('user') != "") { userInfo = UserInfo.fromJson(jsonDecode(value.getString('user'))); + if (userInfo.phone?.isNotEmpty ?? false) { + setState(() {}); + return; + } } if (apiService == null) apiService = ApiService(Dio(), context: context, token: value.getString("token")); From 51ec8411de79d913ecb637a3d02e1ed81d427056 Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 25 Sep 2024 16:41:00 +0800 Subject: [PATCH 48/81] add message conversationId; conversion list refresh sort; message list align top --- lib/im/chat_details_page.dart | 29 +-- lib/im/database/hx_database.dart | 48 +++-- lib/im/database/message.dart | 18 +- lib/im/im_view/im_page.dart | 308 +++++++++++++++---------------- 4 files changed, 206 insertions(+), 197 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 4b41a21a..26ada32c 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -66,6 +67,7 @@ class _ChatDetailsPage extends State int copyIndex = 0; String selfUserId = ""; ImUser _toUser; + String conversation ; @override void onMessage(txt) { @@ -77,14 +79,16 @@ class _ChatDetailsPage extends State int page = 0; loadMessageList() async { - ImUser imUser = await hxDatabase.queryImUserById(_toUser.mid); + selfUserId = (await SharedPreferences.getInstance()).getString("userId"); + conversation = conversationId(selfUserId, _toUser.mid); + + ImUser imUser = await hxDatabase.queryImUserById(conversation); if (imUser == null) { await hxDatabase.insertOrUpdateImUser(_toUser.toJson()); } - selfUserId = (await SharedPreferences.getInstance()).getString("userId"); // unread msg 2 read state - await hxDatabase.readMessage(selfUserId, _toUser.mid); + await hxDatabase.readMessage(conversation); await refresh(); socketClient.addCallback(_toUser.mid, (Message message) { @@ -100,7 +104,7 @@ class _ChatDetailsPage extends State Future refresh() async { List messagePage = - await hxDatabase.queryUList(_toUser.mid, page: page + 1, pageSize: 10); + await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); page += 1; if (page == 1) { messages = messagePage; @@ -118,7 +122,7 @@ class _ChatDetailsPage extends State } Future messageShowTime() async { - List messagePages = await hxDatabase.queryTList(_toUser.mid); + List messagePages = await hxDatabase.queryTList(conversation); for (var value in messages) { Message message = messagePages.firstWhere((element) => value.id == element.id, orElse: () => null); value.showTime = message != null; @@ -318,7 +322,6 @@ class _ChatDetailsPage extends State // setState(() {}); } } - return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -372,9 +375,6 @@ class _ChatDetailsPage extends State footer: CustomFooter( loadStyle: LoadStyle.ShowWhenLoading, builder: (BuildContext context, LoadStatus mode) { - // return (messages.length == 0) - // ? Container() - // : MyFooter(mode); return SizedBox(); }, ), @@ -384,10 +384,13 @@ class _ChatDetailsPage extends State refreshState(); }); }, - child: SingleChildScrollView( + child: Container( + alignment: Alignment.topCenter, + child: SingleChildScrollView( physics: BouncingScrollPhysics(), controller: scrollController, child: chatDetailsList(), + ), ), ), flex: 1, @@ -403,11 +406,12 @@ class _ChatDetailsPage extends State ///聊天列表 Widget chatDetailsList() { return Container( - margin: EdgeInsets.only(bottom: 48.h), + alignment: Alignment.topCenter, child: ListView.builder( itemCount: messages.length, shrinkWrap: true, reverse: true, + padding: EdgeInsets.zero, physics: NeverScrollableScrollPhysics(), itemBuilder: (context, position) { return GestureDetector( @@ -430,7 +434,8 @@ class _ChatDetailsPage extends State bool isText = messages[position].msgType == 1; return Container( padding: EdgeInsets.only( - top: 32.h, + top: 16.h, + bottom: 16.h, ), child: Column( children: [ diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 7e307f57..4f703968 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -22,7 +22,7 @@ class HxDatabase { await openDatabase(databaseName, version: 2, onCreate: (Database db, int version) async { db.execute( - 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `conversationId` VARCHAR(40), `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); db.execute( 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))'); }, onConfigure: (database) async { @@ -49,12 +49,12 @@ class HxDatabase { } } - Future lastMessage(String userId) async { + Future lastMessage(String conversationId) async { await _dbIsOpen(); String sql = - 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT 1'; + 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT 1'; List messages = - await db.rawQuery(sql, [userId, userId]).then((value) { + await db.rawQuery(sql, [conversationId]).then((value) { return value.map((e) { debugPrint("Message: $e"); return Message.fromJson(e); @@ -72,56 +72,54 @@ class HxDatabase { WHERE ROWID IN ( SELECT ROWID FROM ( - SELECT ROWID, `fromId`, `toId`, MAX(`time`) AS max_time + SELECT ROWID, conversationId, MAX(`time`) AS max_time FROM `Message` - GROUP BY `fromId` || '_' || `toId`, `toId` || '_' || `fromId` + GROUP BY conversationId ) AS grouped_messages WHERE max_time = ( SELECT MAX(`time`) FROM `Message` - WHERE (`fromId` = grouped_messages.`fromId` AND `toId` = grouped_messages.`toId`) - OR (`fromId` = grouped_messages.`toId` AND `toId` = grouped_messages.`fromId`) + WHERE conversationId = grouped_messages.conversationId ) ) ORDER BY `time` DESC;'''; return db.rawQuery(sql).then((value) { return value.map((e) { - debugPrint("Message: ${e}"); + debugPrint("Message: $e"); return Message.fromJson(e); }).toList(); }, onError: (error) { - debugPrint("Messageerror: $error"); + debugPrint("MessageError: $error"); }); } - Future> queryUList(userId, {int page = 1, int pageSize = 10}) async{ + Future> queryUList(conversationId, {int page = 1, int pageSize = 10}) async{ await _dbIsOpen(); int start = (page - 1) * pageSize; - String sql = - 'SELECT * FROM Message WHERE toId = ? OR fromId = ? ORDER BY time DESC LIMIT ?, ?'; - return db.rawQuery(sql, [userId, userId, start, pageSize]).then((value) { + String sql = 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT ?, ?'; + return db.rawQuery(sql, [conversationId, start, pageSize]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Messageerror: $error"); }); } - Future> queryTList(userId) async{ + Future> queryTList(conversationId) async{ await _dbIsOpen(); - String sql = 'SELECT *, time / 300000 * 300000 AS time_interval FROM Message WHERE toId = ? OR fromId = ? GROUP BY time_interval ORDER BY time DESC'; - return db.rawQuery(sql, [userId, userId]).then((value) { + String sql = 'SELECT *, time / 300000 * 300000 AS time_interval FROM Message WHERE conversationId = ? GROUP BY time_interval ORDER BY time DESC'; + return db.rawQuery(sql, [conversationId]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Messageerror: $error"); }); } - Future> messageUnreadCount(List userIds, String selfUserId) async { + Future> messageUnreadCount(List conversationIds) async { await _dbIsOpen(); - String userStr = userIds.join(","); + String userStr = conversationIds.join(","); debugPrint("userStr: $userStr"); List messages = await db.rawQuery( - "SELECT * FROM Message WHERE fromId IN ($userStr) AND toId = $selfUserId AND state = 0 AND isDelete = 0", + "SELECT * FROM Message WHERE `conversationId` IN ($userStr) AND state = 0 AND isDelete = 0", ).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { @@ -136,8 +134,8 @@ class HxDatabase { return db.rawQuery(sql); } - Future deleteByUser(String userId) async { - return db.delete("Message",where: "fromId = ? OR toId = ?", whereArgs: [userId, userId]); + Future deleteByUser(String conversationId) async { + return db.delete("Message",where: "conversationId = ?", whereArgs: [conversationId]); } Future deleteAll() async { @@ -158,11 +156,11 @@ class HxDatabase { } /// update message read state - readMessage(String selfUserId, String userId) async{ + readMessage(String conversationId) async{ await _dbIsOpen(); db.update("Message", {"state": 1}, - where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", - whereArgs: [userId, selfUserId]); + where: "conversationId = ? AND state = 0 AND isDelete = 0", + whereArgs: [conversationId]); } Future insertOrUpdateImUser(Map imUserMap) async { diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index c5eadba2..32c6d641 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -1,7 +1,11 @@ +import 'dart:math'; + class Message { int id; + String conversationId; + String fromId; String toId; @@ -23,10 +27,11 @@ class Message { bool showTime = false; - Message(this.id, this.fromId, this.toId, this.replyId, this.content, this.attach, this.msgType, this.time, this.state, this.isDelete); + Message(this.id, this.conversationId, this.fromId, this.toId, this.replyId, this.content, this.attach, this.msgType, this.time, this.state, this.isDelete); factory Message.fromJson(Map json) => Message( json["id"], + json["conversationId"], json["fromId"], json["toId"], json["replyId"], @@ -39,6 +44,7 @@ class Message { Map toJson() => { "id": id, + "conversationId": conversationId, "fromId": fromId, "toId": toId, "replyId": replyId, @@ -53,6 +59,7 @@ class Message { createMessage(var toId, String content, {String attach, int msgType, userId, replyId}) { return { + "conversationId": conversationId(userId, toId), "fromId": userId, "toId": toId, "replyId": replyId, @@ -64,3 +71,12 @@ createMessage(var toId, String content, {String attach, int msgType, userId, rep "isDelete": 0 }; } + +conversationId(tid, fid) { + num itid = num.parse(tid); + num ifid = num.parse(fid); + if (itid > ifid) { + return "$ifid-$itid"; + } + return "$itid-$ifid"; +} diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 724d5ce7..10c313f4 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -43,11 +43,12 @@ class _IMPage extends State implements OnChatMessage { "6": 0, }; int state = 0; - List userIds = []; + List conversationIds = []; Map lastMessageMap = {}; Map unreadCountMap = {}; Map contactMap = {}; int insertIndex = 0; + String selfUserId; @override void onMessage(txt) { @@ -77,14 +78,14 @@ class _IMPage extends State implements OnChatMessage { initSocketClient() async { SharedPreferences shared = await SharedPreferences.getInstance(); - String userId = shared.getString("userId"); - socketClient.addCallback(userId, (Message message) { - if (userIds.contains(message.fromId)) { - userIds.remove(message.fromId); + selfUserId = shared.getString("userId"); + socketClient.addCallback(selfUserId, (Message message) { + if (conversationIds.contains(message.conversationId)) { + conversationIds.remove(message.conversationId); } - userIds.insert(insertIndex, message.fromId); + conversationIds.insert(insertIndex, message.conversationId); - lastMessageMap[message.fromId] = message; + lastMessageMap[message.conversationId] = message; listenerRefresh(message); }); @@ -99,12 +100,9 @@ class _IMPage extends State implements OnChatMessage { } listenerRefresh(Message message) async { - SharedPreferences shared = await SharedPreferences.getInstance(); - String userId = shared.getString("userId"); - await sortConversation(lastMessageMap); - await queryUnreadCount(userIds, userId); + await queryUnreadCount(conversationId); debugPrint("messages_records : ${message.toJson()}"); if (contactMap[message.fromId] == null) { @@ -115,38 +113,35 @@ class _IMPage extends State implements OnChatMessage { } loadMessageList() async { - SharedPreferences shared = await SharedPreferences.getInstance(); - String userId = shared.getString("userId"); messages = await hxDatabase.queryList(); - lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); + lastMessageMap = messages.lGroupBy((p0) => p0.conversationId).mGroupItem(key: (p1) => num.parse(p1.time)); - await sortConversation(lastMessageMap); + await queryImUserInfo(messages.map((e) => e.toId != selfUserId ? e.toId : e.fromId).toList()); - await queryUnreadCount(userIds, userId); + await sortConversation(lastMessageMap); - await queryImUserInfo(userIds); + await queryUnreadCount(conversationIds); refreshState(); } /// update conversation by time sort sortConversation(lastMessageMap) async { - SharedPreferences shared = await SharedPreferences.getInstance(); - String userId = shared.getString("userId"); List sortMessages = lastMessageMap.values.toList(); sortMessages.sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time))); - userIds = sortMessages - .map((e) => e.toId != userId ? e.toId : e.fromId) + conversationIds = sortMessages + .map((e) => e.conversationId) .toSet() - .where((element) => element != userId) .toList(); + conversationIds.forEach((element) { + debugPrint("conversationIds: ${element}"); + }); } /// update conversation unreadcount - queryUnreadCount(userIds, userId) async { - unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); - debugPrint("unreadCount: $unreadCountMap"); + queryUnreadCount(conversationId) async { + unreadCountMap = await hxDatabase.messageUnreadCount(conversationId); } /// update imuser info by mids @@ -162,33 +157,31 @@ class _IMPage extends State implements OnChatMessage { return; } } - contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem(); - List topUserIds = [],notTopUserIds = []; + contactMap = contacts.lGroupBy((p0) => conversationId(p0.mid, selfUserId)).mGroupItem(); + List topConversationIds = [],notTopUserIds = []; contactMap.forEach((key, value) { if(value.isTop == 1) - topUserIds.add(key); + topConversationIds.add(key); else notTopUserIds.add(key); }); - insertIndex = topUserIds.length; - this.userIds = topUserIds..addAll(notTopUserIds); + insertIndex = topConversationIds.length; + this.conversationIds = topConversationIds..addAll(notTopUserIds); } /// update one conversation last message ,and update conversation sort - void updateLastMessage(String userId) async { - Message message = await hxDatabase.lastMessage(userId); - debugPrint("lastmessage: $userId ${message.content} ${message.toJson()}"); + void updateLastMessage(String conversationId) async { + Message message = await hxDatabase.lastMessage(conversationId); + debugPrint("lastmessage: $conversationId ${message.content} ${message.toJson()}"); if (message != null) { - lastMessageMap[userId] = message; + lastMessageMap[conversationId] = message; await sortConversation(lastMessageMap); refreshState(); } } void updateUnreadCount() async { - SharedPreferences shared = await SharedPreferences.getInstance(); - String userId = shared.getString("userId"); - unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); + unreadCountMap = await hxDatabase.messageUnreadCount(conversationIds); refreshState(); } @@ -216,8 +209,8 @@ class _IMPage extends State implements OnChatMessage { await hxDatabase.insertOrUpdateImUser(element.toJson()); }); baseData.data.forEach((element) { - if (contactMap[element.mid] == null) { - contactMap[element.mid] = element; + if (contactMap[conversationId(element.mid, selfUserId)] == null) { + contactMap[conversationId(element.mid, selfUserId)] = element; } }); refreshState(); @@ -233,93 +226,89 @@ class _IMPage extends State implements OnChatMessage { Widget build(BuildContext context) { return Scaffold( backgroundColor: Color(0xFFFFFFFF), - body: SmartRefresher( - enablePullDown: true, - enablePullUp: false, - header: MyHeader(), - physics: BouncingScrollPhysics(), - footer: CustomFooter( - loadStyle: LoadStyle.ShowWhenLoading, - builder: (BuildContext context, LoadStatus mode) { - return (messages.length == 0) ? Container() : MyFooter(mode); - }, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFD9FFDE), + Color(0xFFD9FFDE), + Color(0xFFFFFFFF), + Color(0xFFFFFFFF), + ], + stops: [0, 0.2, 0.4, 1], + ), ), - controller: _refreshController, - onRefresh: _refresh, - onLoading: () { - _refresh(); - }, - child: Container( - // color: Colors.white, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFD9FFDE), - Color(0xFFD9FFDE), - Color(0xFFFFFFFF), - Color(0xFFFFFFFF), - ], - stops: [0, 0.2, 0.4, 1], - ), + child: SmartRefresher( + enablePullDown: true, + enablePullUp: false, + header: MyHeader(), + physics: BouncingScrollPhysics(), + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return (messages.length == 0) ? Container() : MyFooter(mode); + }, ), + controller: _refreshController, + onRefresh: _refresh, + onLoading: () { + _refresh(); + }, child: SingleChildScrollView( physics: BouncingScrollPhysics(), - child: Container( - padding: EdgeInsets.only(bottom: 30.h), - child: Column( - children: [ - Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 12.h, - bottom: 15.h, - right: 16.w, - left: 16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - S.of(context).xiaoxi, - style: TextStyle( - color: Colors.black, - fontSize: 18.sp, - fontWeight: MyFontWeight.bold, - ), + child: Column( + children: [ + Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 12.h, + bottom: 15.h, + right: 16.w, + left: 16.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + S.of(context).xiaoxi, + style: TextStyle( + color: Colors.black, + fontSize: 18.sp, + fontWeight: MyFontWeight.bold, ), ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context) - .pushNamed('/router/chat_friend_group').then((value) { - _refresh(); - }); - }, - child: Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: Color(0xFFFFFFFF), - borderRadius: BorderRadius.circular(20.r), - ), - child: Image.asset( - "assets/image/friend_grouping.webp", - fit: BoxFit.fill, - height: 14.h, - width: 14.h, - ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed('/router/chat_friend_group') + .then((value) { + _refresh(); + }); + }, + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(20.r), + ), + child: Image.asset( + "assets/image/friend_grouping.webp", + fit: BoxFit.fill, + height: 14.h, + width: 14.h, ), ), - ], - ), + ), + ], ), - imPageSearch(), - chatList(), - // buildMessage(),fgg - ], - ), + ), + imPageSearch(), + chatList(), + ], ), ), ), @@ -372,12 +361,13 @@ class _IMPage extends State implements OnChatMessage { Widget chatList() { return Container( child: SlidableAutoCloseBehavior( - child: ListView.builder( + child: ListView( padding: EdgeInsets.only(top: 16), - itemCount: userIds.length, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, position) { + children: conversationIds.map((e) { + debugPrint("---conversationIds: ${e}"); + int position = conversationIds.indexOf(e); return ClipRRect( // borderRadius: BorderRadius.all(Radius.circular(4)), child: Slidable( @@ -388,7 +378,7 @@ class _IMPage extends State implements OnChatMessage { children: [ CustomSlidableAction( onPressed: (bc) { - showDelDialog(userIds[position]); + showDelDialog(conversationIds[position]); }, backgroundColor: Color(0xFFFB312B), foregroundColor: Colors.white, @@ -408,42 +398,41 @@ class _IMPage extends State implements OnChatMessage { ), ], ), - child:GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (userIds[position] == sharedPreferences.getString("userId")) { - SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center); - return; - } - Navigator.of(context).pushNamed( - '/router/chat_details_page', - arguments: { - "toUser": contactMap[userIds[position]], - }, - ).then((value) { - unreadCountMap[userIds[position]] = 0; - updateLastMessage(userIds[position]); - _refresh(); - }); - }, - child: chatItem(userIds[position])), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toUser": contactMap[conversationIds[position]], + }, + ).then((value) { + unreadCountMap[conversationIds[position]] = 0; + updateLastMessage(conversationIds[position]); + _refresh(); + }); + }, + child: chatItem(conversationIds[position]), + ), ), ); - }, + }).toList(), ), ), ); } - Widget chatItem(userId) { + Widget chatItem(conversationId) { return Container( - padding: EdgeInsets.only(left: 16.w, right: 17.w, bottom: 18.h), + padding: EdgeInsets.only( + left: 16.w, right: 17.w, bottom: 18.h, + ), child: Row( children: [ MImage( - !contactMap.containsKey(userId) + !contactMap.containsKey(conversationId) ? null - : contactMap[userId]?.avatar ?? "", + : contactMap[conversationId]?.avatar ?? "", isCircle: true, height: 54.h, width: 54.h, @@ -462,9 +451,9 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - !contactMap.containsKey(userId) + !contactMap.containsKey(conversationId) ? "" - : contactMap[userId]?.nickname ?? "", + : contactMap[conversationId]?.nickname ?? "", // overflow: TextOverflow.fade, maxLines: 1, style: TextStyle( @@ -475,10 +464,10 @@ class _IMPage extends State implements OnChatMessage { ), ), Text( - lastMessageMap[userId]?.time != null + lastMessageMap[conversationId]?.time != null ? AppUtils.timeFormatter( DateTime.fromMillisecondsSinceEpoch(num.parse( - lastMessageMap[userId]?.time ?? ""))) + lastMessageMap[conversationId]?.time ?? ""))) : "", style: TextStyle( fontSize: 12.sp, @@ -495,7 +484,7 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - lastMessageMap[userId]?.content ?? "", + lastMessageMap[conversationId]?.content ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -505,7 +494,7 @@ class _IMPage extends State implements OnChatMessage { ), ), ), - if (unreadCountMap[userId] != null && unreadCountMap[userId] > 0) + if (unreadCountMap[conversationId] != null && unreadCountMap[conversationId] > 0) Container( width: 16, height: 16, @@ -514,7 +503,7 @@ class _IMPage extends State implements OnChatMessage { color: Color(0xFFFF441A), ), child: RoundButton( - text: "${unreadCountMap[userId]}", + text: "${unreadCountMap[conversationId]}", textColor: Colors.white, fontWeight: MyFontWeight.regular, backgroup: Color(0xFFFF441A), @@ -534,7 +523,7 @@ class _IMPage extends State implements OnChatMessage { ///确认删除弹窗 - showDelDialog(userId) { + showDelDialog(conversationId) { showDialog( context: context, builder: (context) { @@ -560,7 +549,8 @@ class _IMPage extends State implements OnChatMessage { fontWeight: MyFontWeight.bold, ), ), - )), + ), + ), // Spacer(), Container( height:1.h, @@ -596,7 +586,7 @@ class _IMPage extends State implements OnChatMessage { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async{ - await hxDatabase.deleteByUser(contactMap[userId]?.mid ?? ""); + await hxDatabase.deleteByUser(conversationId); _refresh(); Navigator.of(context).pop(); }, @@ -605,10 +595,10 @@ class _IMPage extends State implements OnChatMessage { textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFFF370A), - ) - ) - ) - ) + ), + ), + ), + ), ], ) ], From a4206f9b9d89f7cc1cb6dc73ffdba3f995571593 Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 25 Sep 2024 19:11:10 +0800 Subject: [PATCH 49/81] chat add conversation ,database query change userId to conversationId --- lib/im/chat_details_page.dart | 306 +++++++++++++++++----------------- 1 file changed, 154 insertions(+), 152 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 26ada32c..3b2684d9 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -67,7 +67,7 @@ class _ChatDetailsPage extends State int copyIndex = 0; String selfUserId = ""; ImUser _toUser; - String conversation ; + String conversation; @override void onMessage(txt) { @@ -124,7 +124,8 @@ class _ChatDetailsPage extends State Future messageShowTime() async { List messagePages = await hxDatabase.queryTList(conversation); for (var value in messages) { - Message message = messagePages.firstWhere((element) => value.id == element.id, orElse: () => null); + Message message = messagePages + .firstWhere((element) => value.id == element.id, orElse: () => null); value.showTime = message != null; } } @@ -142,8 +143,10 @@ class _ChatDetailsPage extends State } } if (apiService == null) - apiService = ApiService(Dio(), context: context, token: value.getString("token")); - BaseData baseData = await apiService.queryInfo().catchError((onError) {}); + apiService = + ApiService(Dio(), context: context, token: value.getString("token")); + BaseData baseData = + await apiService.queryInfo().catchError((onError) {}); if (baseData != null && baseData.isSuccess) { setState(() { userInfo = baseData.data; @@ -323,83 +326,83 @@ class _ChatDetailsPage extends State } } return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - FocusScope.of(context).requestFocus(FocusNode()); - setState(() { - emojiShowing = false; - isKeyBoardShow = emojiShowing; - moreShow = false; - isKeyBoardShow = moreShow; - }); - }, - child: Scaffold( - // resizeToAvoidBottomInset: false, - backgroundColor: Color(0xFFF6F6F6), - appBar: MyAppBar( - title: _toUser.nickname, - titleColor: Color(0xFF0D0D0D), - titleSize: 17.sp, - leading: true, - leadingColor: Colors.black, - action: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - await Navigator.of(context).pushNamed('/router/chat_setting', - arguments: {"userId": _toUser.mid}); - page = 0; - refresh().then((value) { - refreshState(); - }); - }, - child: Container( - alignment: Alignment.center, - child: Icon( - Icons.more_horiz, - color: Colors.black, - size: 30, - ), + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + setState(() { + emojiShowing = false; + isKeyBoardShow = emojiShowing; + moreShow = false; + isKeyBoardShow = moreShow; + }); + }, + child: Scaffold( + // resizeToAvoidBottomInset: false, + backgroundColor: Color(0xFFF6F6F6), + appBar: MyAppBar( + title: _toUser.nickname, + titleColor: Color(0xFF0D0D0D), + titleSize: 17.sp, + leading: true, + leadingColor: Colors.black, + action: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + await Navigator.of(context).pushNamed('/router/chat_setting', + arguments: {"userId": _toUser.mid}); + page = 0; + refresh().then((value) { + refreshState(); + }); + }, + child: Container( + alignment: Alignment.center, + child: Icon( + Icons.more_horiz, + color: Colors.black, + size: 30, ), ), ), - body: Container( - child: Column( - children: [ - Expanded( - child: SmartRefresher( - enablePullDown: false, - enablePullUp: true, - header: MyHeader(), - reverse: true, - physics: BouncingScrollPhysics(), - footer: CustomFooter( - loadStyle: LoadStyle.ShowWhenLoading, - builder: (BuildContext context, LoadStatus mode) { - return SizedBox(); - }, - ), - controller: refreshController, - onLoading: () { - refresh().then((value) { - refreshState(); - }); + ), + body: Container( + child: Column( + children: [ + Expanded( + child: SmartRefresher( + enablePullDown: false, + enablePullUp: true, + header: MyHeader(), + reverse: true, + physics: BouncingScrollPhysics(), + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return SizedBox(); }, - child: Container( - alignment: Alignment.topCenter, - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - child: chatDetailsList(), - ), + ), + controller: refreshController, + onLoading: () { + refresh().then((value) { + refreshState(); + }); + }, + child: Container( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: scrollController, + child: chatDetailsList(), ), ), - flex: 1, ), - input() - ], - ), + flex: 1, + ), + input() + ], ), ), + ), ); } @@ -444,12 +447,12 @@ class _ChatDetailsPage extends State // position == messages.length-1 // ? AppUtils.milliTimeFormatter(DateTime.fromMillisecondsSinceEpoch( - int.parse(messages[position].time))) - // : AppUtils.getTimeDisplay( - // DateTime.fromMillisecondsSinceEpoch( - // int.parse(messages[position].time)), - // DateTime.fromMillisecondsSinceEpoch( - // int.parse(messages[position+1].time))) + int.parse(messages[position].time))) + // : AppUtils.getTimeDisplay( + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position].time)), + // DateTime.fromMillisecondsSinceEpoch( + // int.parse(messages[position+1].time))) , textAlign: TextAlign.center, style: TextStyle( @@ -1139,84 +1142,83 @@ class _ChatDetailsPage extends State context: context, builder: (BuildContext context) { return AlertDialog( - // backgroundColor: Color(0xFF2A2A2A), - // elevation: 0, - contentPadding: EdgeInsets.only( - top: 8.h, - bottom: 5.h, - ), - content: Container( - width: 160.w, - height:50.h, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric( - horizontal: 32.w, - vertical: 7.5.h, + // backgroundColor: Color(0xFF2A2A2A), + // elevation: 0, + contentPadding: EdgeInsets.only( + top: 8.h, + bottom: 5.h, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - copyIndex = 0; - this.copy(tex); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + content: Container( + width: 160.w, + height: 50.h, + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(6), + ), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 7.5.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() { + copyIndex = 0; + this.copy(tex); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, ), - ), - ], - ), - ), - GestureDetector( - onTap: () {}, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + SizedBox( + height: 2.h, ), - ), - ], + Text( + "复制", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), ), - ) - ], - ), - ) - ); + GestureDetector( + onTap: () {}, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ) + ], + ), + )); }); } From 6867bb83ecdb74f5c5f191f1b107e8407e7dc5d4 Mon Sep 17 00:00:00 2001 From: zsw Date: Wed, 25 Sep 2024 21:01:45 +0800 Subject: [PATCH 50/81] chat add conversation ,database query change userId to conversationId --- lib/im/chat_details_page.dart | 1 + lib/im/im_view/im_page.dart | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 3b2684d9..9647face 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -330,6 +330,7 @@ class _ChatDetailsPage extends State onTap: () { FocusScope.of(context).requestFocus(FocusNode()); setState(() { + needShowSmiley = false; emojiShowing = false; isKeyBoardShow = emojiShowing; moreShow = false; diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 10c313f4..c0d0612e 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -102,7 +102,7 @@ class _IMPage extends State implements OnChatMessage { listenerRefresh(Message message) async { await sortConversation(lastMessageMap); - await queryUnreadCount(conversationId); + await queryUnreadCount(conversationIds); debugPrint("messages_records : ${message.toJson()}"); if (contactMap[message.fromId] == null) { @@ -134,14 +134,11 @@ class _IMPage extends State implements OnChatMessage { .map((e) => e.conversationId) .toSet() .toList(); - conversationIds.forEach((element) { - debugPrint("conversationIds: ${element}"); - }); } /// update conversation unreadcount - queryUnreadCount(conversationId) async { - unreadCountMap = await hxDatabase.messageUnreadCount(conversationId); + queryUnreadCount(conversationIds) async { + unreadCountMap = await hxDatabase.messageUnreadCount(conversationIds); } /// update imuser info by mids @@ -366,7 +363,6 @@ class _IMPage extends State implements OnChatMessage { shrinkWrap: true, physics: NeverScrollableScrollPhysics(), children: conversationIds.map((e) { - debugPrint("---conversationIds: ${e}"); int position = conversationIds.indexOf(e); return ClipRRect( // borderRadius: BorderRadius.all(Radius.circular(4)), From 51aeca7b2ee02afa3ecf5743c89338f40323613e Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Thu, 26 Sep 2024 10:21:01 +0800 Subject: [PATCH 51/81] =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=B8=BB=E9=A1=B5?= =?UTF-8?q?=E5=85=B3=E6=B3=A8=E6=8C=89=E9=92=AE=E7=82=B9=E5=87=BB=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=A4=84=E7=90=86=EF=BC=9B=20=E5=85=B3=E6=B3=A8?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=A4=84=E7=90=86=EF=BC=9B=20=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E9=A1=B5=E9=9D=A2=E8=81=8A=E5=A4=A9=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E8=B7=9F=E5=A4=B4=E5=83=8F=E4=BD=8D=E7=BD=AE=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 13 ++++++------- lib/mine/personal_page.dart | 11 ++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 26ada32c..a041e4cc 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/im/im_view/w_popup_menu.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; @@ -569,6 +570,7 @@ class _ChatDetailsPage extends State Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( behavior: HitTestBehavior.opaque, @@ -727,6 +729,7 @@ class _ChatDetailsPage extends State Padding( padding: EdgeInsets.only(left: 36.w, right: 16.w), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (messages[position].state == 3) Container( @@ -1139,14 +1142,10 @@ class _ChatDetailsPage extends State context: context, builder: (BuildContext context) { return AlertDialog( - // backgroundColor: Color(0xFF2A2A2A), - // elevation: 0, - contentPadding: EdgeInsets.only( - top: 8.h, - bottom: 5.h, - ), + backgroundColor: Colors.transparent, + elevation: 0, content: Container( - width: 160.w, + // width: 60.w, height:50.h, decoration: BoxDecoration( color: Color(0xFF2A2A2A), diff --git a/lib/mine/personal_page.dart b/lib/mine/personal_page.dart index af905b6c..4db5d6c7 100644 --- a/lib/mine/personal_page.dart +++ b/lib/mine/personal_page.dart @@ -29,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../retrofit/data/im_user.dart'; import '../view_widget/my_tab.dart'; +import '../view_widget/settlement_tips_dialog.dart'; class PersonalPage extends StatefulWidget { final Map arguments; @@ -115,8 +116,11 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing BaseData baseData = await apiService.follow(followId ?? ""); if (baseData != null && baseData.isSuccess) { queryCommunity(null); - SmartDialog.showToast(isFollow ? "关注成功" : "取关成功", - alignment: Alignment.center); + SmartDialog.show( + widget: SettlementTips( + () {}, + text: isFollow ? "取关成功" : "关注成功" + )); setState(() {}); } else { SmartDialog.showToast(baseData.msg, alignment: Alignment.center); @@ -1260,7 +1264,8 @@ class _PersonalPage extends State with WidgetsBindingObserver,Sing Expanded(child:GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - + if (articles.isNotEmpty) + vipFollow(memberId, articles.first.followed ?? false); }, child: Container( // padding: EdgeInsets.symmetric(vertical:12.h), From 5e5ab323ca796ce6236127b83795be022ea24359 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 26 Sep 2024 18:04:52 +0800 Subject: [PATCH 52/81] message page --- lib/im/SocketClient.dart | 20 ++++++-- lib/im/chat_details_page.dart | 81 +++++++++++++++----------------- lib/im/chat_setting.dart | 10 ++-- lib/im/database/hx_database.dart | 33 +++++++------ lib/im/database/message.dart | 41 +++++++++++++++- lib/im/im_view/im_page.dart | 3 +- lib/retrofit/min_api.dart | 6 +-- lib/retrofit/retrofit_api.dart | 4 +- lib/utils/upload_async.dart | 7 +-- 9 files changed, 126 insertions(+), 79 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 0f169bf1..0396afcd 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -16,7 +16,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; + final String ip = !kDebugMode ? '192.168.10.129' : '47.93.216.24'; final num port = 9090; Socket _socket; SharedPreferences _shared; @@ -42,7 +42,8 @@ class SocketClient { MsgData dataResult = MsgData.fromBuffer(proto.body); print('收到来自:${dataResult.from},消息内容: ${utf8.decode(dataResult.data)} '); - Map messageMap = createMessage(userId, utf8.decode(dataResult.data), msgType: dataResult.type.value, userId: dataResult.from); + Map messageMap = createMessage(userId, dataResult.data, msgType: dataResult.type.value, userId: dataResult.from); + if (callbacks[dataResult.from] != null) { messageMap["state"] = 1; } @@ -54,6 +55,7 @@ class SocketClient { } callbacks[userId]?.call(message); /// user self conversation list callback }); + }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); showDebugToast("socket-listen-error: $error, stackTrace: $stackTrace"); @@ -152,17 +154,26 @@ class SocketClient { if (!checkSocket()) { return; } + debugPrint("socket-authRequest: request"); final authReq = AuthReq() ..uid = userId ..token = token; final authReqBytes = authReq.writeToBuffer(); final proto = Proto(1, 1, authReqBytes); // 假设 operation 和 seqId 为 1 final protoBytes = proto.toBytes(); - _socket.add(protoBytes); + try { + _socket.add(protoBytes); + } catch (e) { + debugPrint("socket-authRequest: $e"); + Future.delayed(const Duration(milliseconds: 1000), () { + authRequest(token); + }); + } } Future sendMessage(String toId, String content) async { - Map message = createMessage(toId, content, userId: userId); + Map message = createSendMessage(toId, content, userId: userId); + message["state"] = 1; int id = await hxDatabase.insert(message).catchError((error) { debugPrint("insertMessage: $error"); }); @@ -188,7 +199,6 @@ class SocketClient { showDebugToast("socket-send-error: ${e.toString()}"); reconnect(); } - debugPrint("sendMessage: ${message["id"]}"); return Message.fromJson(message); } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index dd072390..859e4c20 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -105,19 +105,19 @@ class _ChatDetailsPage extends State Future refresh() async { List messagePage = await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); - page += 1; - if (page == 1) { - messages = messagePage; - } else { - messages.addAll(messagePage); - } - await messageShowTime(); if (messagePage.isEmpty) { refreshController.loadNoData(); return; } else { refreshController.loadComplete(); + page += 1; } + if (page == 1) { + messages = messagePage; + } else { + messages.addAll(messagePage); + } + await messageShowTime(); return Future.value(); } @@ -177,7 +177,7 @@ class _ChatDetailsPage extends State void jumpToBottom() { Future.delayed(const Duration(milliseconds: 100), () { scrollController.position - .jumpTo(scrollController.position.maxScrollExtent); + .jumpTo(0); }); } @@ -374,32 +374,28 @@ class _ChatDetailsPage extends State enablePullDown: false, enablePullUp: true, header: MyHeader(), - reverse: true, - physics: BouncingScrollPhysics(), footer: CustomFooter( loadStyle: LoadStyle.ShowWhenLoading, builder: (BuildContext context, LoadStatus mode) { return SizedBox(); }, ), + reverse: true, + scrollController: scrollController, controller: refreshController, + onRefresh: () { + debugPrint(""); + }, onLoading: () { refresh().then((value) { refreshState(); }); }, - child: Container( - alignment: Alignment.topCenter, - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - child: chatDetailsList(), - ), - ), + child: chatDetailsList(), ), flex: 1, ), - input() + input(), ], ), ), @@ -409,27 +405,22 @@ class _ChatDetailsPage extends State ///聊天列表 Widget chatDetailsList() { - return Container( - alignment: Alignment.topCenter, - child: ListView.builder( - itemCount: messages.length, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, position) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - copyIndex = 0; - FocusScope.of(context).requestFocus(FocusNode()); - }); - }, - child: chatDetailsItem(position), - ); - }, - ), + return ListView.builder( + itemCount: messages.length, + reverse: true, + padding: EdgeInsets.zero, + itemBuilder: (context, position) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + copyIndex = 0; + FocusScope.of(context).requestFocus(FocusNode()); + }); + }, + child: chatDetailsItem(position), + ); + }, ); } @@ -936,8 +927,10 @@ class _ChatDetailsPage extends State Message message = value; messages.insert(0, message); chatController.clear(); - refreshState(); - jumpToBottom(); + messageShowTime().then((value) { + refreshState(); + jumpToBottom(); + }); }); }, maxLines: 8, @@ -1028,7 +1021,8 @@ class _ChatDetailsPage extends State buttonMode: ButtonMode.MATERIAL, checkPlatformCompatibility: true, ), - )), + ), + ), ), ///更多 @@ -1227,4 +1221,5 @@ class _ChatDetailsPage extends State print(tex); Clipboard.setData(ClipboardData(text: tex)); } + } diff --git a/lib/im/chat_setting.dart b/lib/im/chat_setting.dart index 75dbe0f9..5f8654ce 100644 --- a/lib/im/chat_setting.dart +++ b/lib/im/chat_setting.dart @@ -5,9 +5,11 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../main.dart'; @@ -26,6 +28,7 @@ class ChatSetting extends StatefulWidget { class _ChatSetting extends State { ImUser imUser; + String selfUserId = ""; @override void initState() { @@ -34,6 +37,7 @@ class _ChatSetting extends State { } void initData() async { + selfUserId = (await SharedPreferences.getInstance()).getString("userId"); imUser = await hxDatabase.queryImUserById(widget.arguments["userId"]); setState(() { @@ -79,8 +83,7 @@ class _ChatSetting extends State { onChanged: (bool value) async { if (imUser == null) return; imUser.isTop = value ? 1 : 0; - await hxDatabase - .insertOrUpdateImUser(imUser.toJson()); + await hxDatabase.insertOrUpdateImUser(imUser.toJson()); setState(() {}); }, ), @@ -112,8 +115,7 @@ class _ChatSetting extends State { ], ), onTap: () async { - await hxDatabase - .deleteByUser(widget.arguments["userId"] ?? ""); + await hxDatabase.deleteByUser(conversationId(widget.arguments["userId"] ?? "", selfUserId)); // SmartDialog.showToast("删除成功", // alignment: Alignment.center); }, diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 4f703968..585be2e9 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -11,15 +11,15 @@ class HxDatabase { Database db; void open({String key}) async { - // _migrations.add(Migration(1, 2, (Database database) async { - // database.execute('ALTER TABLE `Message` ADD COLUMN `replyId` VARCHAR(20) DEFAULT NULL AFTER `toId`'); + // _migrations.add(Migration(3, 4, (Database database) async { + // await database.execute('ALTER TABLE ImUser ADD COLUMN IF NOT EXISTS `isTop` INTEGER DEFAULT 0'); // })); String databaseName = 'hx.db'; if (key?.isNotEmpty ?? false) { databaseName = 'hx_$key.db'; } - await openDatabase(databaseName, version: 2, + await openDatabase(databaseName, version: 1, onCreate: (Database db, int version) async { db.execute( 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `conversationId` VARCHAR(40), `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); @@ -27,10 +27,7 @@ class HxDatabase { 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))'); }, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); - try { - await database.execute( - 'ALTER TABLE ImUser ADD COLUMN isTop INTEGER DEFAULT 0'); - }catch (e){} + debugPrint("database-version: ${await database.getVersion()}"); }, onUpgrade: (database, startVersion, endVersion) async { await runMigrations(database, startVersion, endVersion, _migrations); }, onOpen: (Database db) { @@ -51,10 +48,8 @@ class HxDatabase { Future lastMessage(String conversationId) async { await _dbIsOpen(); - String sql = - 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT 1'; - List messages = - await db.rawQuery(sql, [conversationId]).then((value) { + String sql = 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT 1'; + List messages = await db.rawQuery(sql, [conversationId]).then((value) { return value.map((e) { debugPrint("Message: $e"); return Message.fromJson(e); @@ -89,7 +84,7 @@ class HxDatabase { return Message.fromJson(e); }).toList(); }, onError: (error) { - debugPrint("MessageError: $error"); + debugPrint("Message-error: $error"); }); } @@ -100,7 +95,7 @@ class HxDatabase { return db.rawQuery(sql, [conversationId, start, pageSize]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { - debugPrint("Messageerror: $error"); + debugPrint("Message-error: $error"); }); } @@ -110,7 +105,7 @@ class HxDatabase { return db.rawQuery(sql, [conversationId]).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { - debugPrint("Messageerror: $error"); + debugPrint("Message-error: $error"); }); } @@ -118,14 +113,18 @@ class HxDatabase { await _dbIsOpen(); String userStr = conversationIds.join(","); debugPrint("userStr: $userStr"); - List messages = await db.rawQuery( - "SELECT * FROM Message WHERE `conversationId` IN ($userStr) AND state = 0 AND isDelete = 0", + List messages = await db.query("Message", + where: "`conversationId` IN (?) AND state = 0 AND isDelete = 0", + whereArgs: [userStr], ).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Message-error: $error"); }); - return (messages??[]).lGroupBy((p) => p.fromId).mGroupCount; + messages.forEach((element) { + debugPrint("messageUnreadCount: ${element.toJson()}"); + }); + return (messages??[]).lGroupBy((p) => p.conversationId).mGroupCount; } Future> queryListAll() async{ diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 32c6d641..9f5b13c1 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -1,5 +1,12 @@ +import 'dart:convert'; import 'dart:math'; +import 'dart:typed_data'; + +import 'package:flutter/cupertino.dart'; +import 'package:huixiang/im/out/message.pb.dart'; +import 'package:huixiang/im/out/message.pbenum.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; class Message { int id; @@ -57,7 +64,39 @@ class Message { }; } -createMessage(var toId, String content, {String attach, int msgType, userId, replyId}) { +createSendMessage(var toId, String content, {String attach, int msgType, userId, replyId}) { + return { + "conversationId": conversationId(userId, toId), + "fromId": userId, + "toId": toId, + "replyId": replyId, + "content": content, + "attach": attach, + "msgType": msgType ?? 1, + "time": "${DateTime.now().millisecondsSinceEpoch}", + "state": 0, + "isDelete": 0 + }; +} + +createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) { + String content = ""; + MsgType type = MsgType.values[msgType]; + if (type == MsgType.TEXT) { + content = utf8.decode(dataBytes); + } + if (type == MsgType.IMAGE || type == MsgType.AUDIO || type == MsgType.VIDEO) { + Map result = ImageGallerySaver.saveImage( + dataBytes, + isReturnImagePathOfIOS: true, + ); + bool isSuccess = result["isSuccess"] != null && result["isSuccess"]; + if (isSuccess) { + attach = result["filePath"]; + } + } + + return { "conversationId": conversationId(userId, toId), "fromId": userId, diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index c0d0612e..f775ae7c 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -139,6 +139,7 @@ class _IMPage extends State implements OnChatMessage { /// update conversation unreadcount queryUnreadCount(conversationIds) async { unreadCountMap = await hxDatabase.messageUnreadCount(conversationIds); + debugPrint("unreadCountMap: $unreadCountMap"); } /// update imuser info by mids @@ -169,7 +170,7 @@ class _IMPage extends State implements OnChatMessage { /// update one conversation last message ,and update conversation sort void updateLastMessage(String conversationId) async { Message message = await hxDatabase.lastMessage(conversationId); - debugPrint("lastmessage: $conversationId ${message.content} ${message.toJson()}"); + debugPrint("lastmessage: $conversationId ${message?.content} ${message?.toJson()}"); if (message != null) { lastMessageMap[conversationId] = message; await sortConversation(lastMessageMap); diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index 0115d920..f34e728b 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -27,8 +27,9 @@ import 'data/shopping_home_config.dart'; part 'min_api.g.dart'; -const localBaseUrl = "http://192.168.10.54:8765/app/";///本地 +// const localBaseUrl = "http://192.168.10.54:8765/app/";///本地 // const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///测试 +const localBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 const serviceBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 @@ -44,8 +45,7 @@ abstract class MinApiService { String storeId, bool showLoading = false, }) { - Map headers = - (token == null || token == "") ? {} : {'token': "Bearer $token"}; + Map headers = (token == null || token == "") ? {} : {'token': "Bearer $token"}; if (tenant != null && tenant != "") { headers["tenant"] = tenant; } diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index a7a07276..e569623f 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -70,13 +70,13 @@ import 'data/wx_pay.dart'; part 'retrofit_api.g.dart'; -const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 +// const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 // const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///测试 +const localBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 const ipBaseUrl = "http://whois.pconline.com.cn"; -///ip @RestApi(baseUrl: localBaseUrl) abstract class ApiService { diff --git a/lib/utils/upload_async.dart b/lib/utils/upload_async.dart index 14188069..d1e62507 100644 --- a/lib/utils/upload_async.dart +++ b/lib/utils/upload_async.dart @@ -34,7 +34,8 @@ class UploadAsync { decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(2)), child: dynamicType == 0 - ? Expanded(child: Column( + ? Expanded( + child: Column( children: [ Text( dynamicText, @@ -49,8 +50,8 @@ class UploadAsync { Expanded(child: Container( height: 20.h, - )) - ])) + ),), + ],),) : ClipRRect( borderRadius: BorderRadius.circular(2), child: Image.file( From 5f2e1c5d1a15944fd1a835545972466de507098a Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 26 Sep 2024 18:25:18 +0800 Subject: [PATCH 53/81] message page --- lib/im/database/message.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 9f5b13c1..d25d8b64 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -79,7 +79,7 @@ createSendMessage(var toId, String content, {String attach, int msgType, userId, }; } -createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) { +createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) ccc{ String content = ""; MsgType type = MsgType.values[msgType]; if (type == MsgType.TEXT) { @@ -96,7 +96,6 @@ createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId } } - return { "conversationId": conversationId(userId, toId), "fromId": userId, From fa83bc4c1d2522265956237a7d66f8abbf6ab399 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 26 Sep 2024 18:26:31 +0800 Subject: [PATCH 54/81] message page --- lib/im/SocketClient.dart | 2 +- lib/im/database/message.dart | 2 +- lib/retrofit/min_api.dart | 3 +-- lib/retrofit/retrofit_api.dart | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 0396afcd..757e808e 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -16,7 +16,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - final String ip = !kDebugMode ? '192.168.10.129' : '47.93.216.24'; + final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; final num port = 9090; Socket _socket; SharedPreferences _shared; diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index d25d8b64..0ff06e5f 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -79,7 +79,7 @@ createSendMessage(var toId, String content, {String attach, int msgType, userId, }; } -createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) ccc{ +createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) { String content = ""; MsgType type = MsgType.values[msgType]; if (type == MsgType.TEXT) { diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index f34e728b..d454a7eb 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -27,9 +27,8 @@ import 'data/shopping_home_config.dart'; part 'min_api.g.dart'; -// const localBaseUrl = "http://192.168.10.54:8765/app/";///本地 +const localBaseUrl = "http://192.168.10.54:8765/app/";///本地 // const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///测试 -const localBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 const serviceBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index e569623f..a7a07276 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -70,13 +70,13 @@ import 'data/wx_pay.dart'; part 'retrofit_api.g.dart'; -// const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 +const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 // const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///测试 -const localBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 const ipBaseUrl = "http://whois.pconline.com.cn"; +///ip @RestApi(baseUrl: localBaseUrl) abstract class ApiService { From a464edd2f1a453cb54647254ddc1203ccc10e703 Mon Sep 17 00:00:00 2001 From: zsw Date: Thu, 26 Sep 2024 18:29:39 +0800 Subject: [PATCH 55/81] message page --- lib/im/chat_details_page.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 859e4c20..66a9486a 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; @@ -966,8 +967,9 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - _onMoreTap(); - jumpToBottom(); + // _onMoreTap(); + // jumpToBottom(); + SmartDialog.showToast("暂不支持", alignment: Alignment.center); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), From c0404b659980187cffe6ef164293064bd819eb03 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 10:37:42 +0800 Subject: [PATCH 56/81] message page --- lib/im/SocketClient.dart | 2 +- lib/im/database/hx_database.dart | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 757e808e..0396afcd 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -16,7 +16,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; + final String ip = !kDebugMode ? '192.168.10.129' : '47.93.216.24'; final num port = 9090; Socket _socket; SharedPreferences _shared; diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 585be2e9..4daa8573 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -19,12 +19,9 @@ class HxDatabase { if (key?.isNotEmpty ?? false) { databaseName = 'hx_$key.db'; } - await openDatabase(databaseName, version: 1, - onCreate: (Database db, int version) async { - db.execute( - 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `conversationId` VARCHAR(40), `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); - db.execute( - 'CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))'); + await openDatabase(databaseName, version: 1, onCreate: (Database db, int version) async { + db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `conversationId` VARCHAR(40), `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); + db.execute('CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))'); }, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); debugPrint("database-version: ${await database.getVersion()}"); @@ -111,19 +108,16 @@ class HxDatabase { Future> messageUnreadCount(List conversationIds) async { await _dbIsOpen(); - String userStr = conversationIds.join(","); + String userStr = conversationIds.join("','"); debugPrint("userStr: $userStr"); List messages = await db.query("Message", - where: "`conversationId` IN (?) AND state = 0 AND isDelete = 0", - whereArgs: [userStr], + where: "conversationId IN ('$userStr') AND state = 0 AND isDelete = 0", + whereArgs: [], ).then((value) { return value.map((e) => Message.fromJson(e)).toList(); }, onError: (error) { debugPrint("Message-error: $error"); }); - messages.forEach((element) { - debugPrint("messageUnreadCount: ${element.toJson()}"); - }); return (messages??[]).lGroupBy((p) => p.conversationId).mGroupCount; } From ed1ec5bc50857d5b8efc261b88b9d106c9bd48d4 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 10:38:22 +0800 Subject: [PATCH 57/81] message page --- lib/im/SocketClient.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 0396afcd..757e808e 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -16,7 +16,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - final String ip = !kDebugMode ? '192.168.10.129' : '47.93.216.24'; + final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; final num port = 9090; Socket _socket; SharedPreferences _shared; From 77a759f06a14c334a5c5bb17f381f95e098c0d1f Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 15:41:45 +0800 Subject: [PATCH 58/81] message type --- lib/im/SocketClient.dart | 61 +++++++++++++++++++++++++++++------- lib/im/database/message.dart | 38 +++------------------- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 757e808e..1ea16362 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -11,6 +11,8 @@ import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/out/auth.pb.dart'; import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { @@ -42,20 +44,19 @@ class SocketClient { MsgData dataResult = MsgData.fromBuffer(proto.body); print('收到来自:${dataResult.from},消息内容: ${utf8.decode(dataResult.data)} '); - Map messageMap = createMessage(userId, dataResult.data, msgType: dataResult.type.value, userId: dataResult.from); - - if (callbacks[dataResult.from] != null) { - messageMap["state"] = 1; - } - hxDatabase.insert(messageMap).then((value) { - messageMap["id"] = value; - Message message = Message.fromJson(messageMap); + receiveInsertMessage(dataResult).then((messageMap) { if (callbacks[dataResult.from] != null) { - callbacks[dataResult.from].call(message); /// user conversation callback + messageMap["state"] = 1; } - callbacks[userId]?.call(message); /// user self conversation list callback + hxDatabase.insert(messageMap).then((value) { + messageMap["id"] = value; + Message message = Message.fromJson(messageMap); + if (callbacks[dataResult.from] != null) { + callbacks[dataResult.from].call(message); /// user conversation callback + } + callbacks[userId]?.call(message); /// user self conversation list callback + }); }); - }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); showDebugToast("socket-listen-error: $error, stackTrace: $stackTrace"); @@ -75,6 +76,42 @@ class SocketClient { }); } + Future> receiveInsertMessage(MsgData dataResult) async { + String content = ""; + String attach = ""; + Uint8List dataBytes = dataResult.data; + MsgType type = MsgType.values[dataResult.type.value]; + if (type == MsgType.TEXT) { + content = utf8.decode(dataBytes); + } else if (type == MsgType.IMAGE || type == MsgType.VIDEO) { + Map result = await ImageGallerySaver.saveImage( + dataBytes, + isReturnImagePathOfIOS: true, + ); + bool isSuccess = result["isSuccess"] != null && result["isSuccess"]; + if (isSuccess) { + attach = result["filePath"]; + } + } else if (type == MsgType.AUDIO) { + Directory dir = await getTemporaryDirectory(); + File file = File("${dir.path}/hx_${DateTime.now().millisecondsSinceEpoch}.wav"); + try { + IOSink ioSink = file.openWrite(); + ioSink.write(dataBytes); + ioSink.close(); + attach = file.path; + } catch (e) { + debugPrint("${file.path} write fail"); + } + } else { + content = "暂不支持的消息格式"; + } + + Map messageMap = createMessage(userId, content, attach: attach, msgType: type.value, fromId: dataResult.from); + + return messageMap; + } + showDebugToast(text) { if (kDebugMode) { SmartDialog.showToast(text, alignment: Alignment.center); @@ -172,7 +209,7 @@ class SocketClient { } Future sendMessage(String toId, String content) async { - Map message = createSendMessage(toId, content, userId: userId); + Map message = createMessage(toId, content, fromId: userId); message["state"] = 1; int id = await hxDatabase.insert(message).catchError((error) { debugPrint("insertMessage: $error"); diff --git a/lib/im/database/message.dart b/lib/im/database/message.dart index 0ff06e5f..32b19389 100644 --- a/lib/im/database/message.dart +++ b/lib/im/database/message.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; @@ -7,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/im/out/message.pbenum.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; class Message { int id; @@ -64,10 +66,10 @@ class Message { }; } -createSendMessage(var toId, String content, {String attach, int msgType, userId, replyId}) { +createMessage(var toId, String content, {String attach, int msgType, fromId, replyId}) { return { - "conversationId": conversationId(userId, toId), - "fromId": userId, + "conversationId": conversationId(fromId, toId), + "fromId": fromId, "toId": toId, "replyId": replyId, "content": content, @@ -79,36 +81,6 @@ createSendMessage(var toId, String content, {String attach, int msgType, userId, }; } -createMessage(var toId, Uint8List dataBytes, {String attach, int msgType, userId, replyId}) { - String content = ""; - MsgType type = MsgType.values[msgType]; - if (type == MsgType.TEXT) { - content = utf8.decode(dataBytes); - } - if (type == MsgType.IMAGE || type == MsgType.AUDIO || type == MsgType.VIDEO) { - Map result = ImageGallerySaver.saveImage( - dataBytes, - isReturnImagePathOfIOS: true, - ); - bool isSuccess = result["isSuccess"] != null && result["isSuccess"]; - if (isSuccess) { - attach = result["filePath"]; - } - } - - return { - "conversationId": conversationId(userId, toId), - "fromId": userId, - "toId": toId, - "replyId": replyId, - "content": content, - "attach": attach, - "msgType": msgType ?? 1, - "time": "${DateTime.now().millisecondsSinceEpoch}", - "state": 0, - "isDelete": 0 - }; -} conversationId(tid, fid) { num itid = num.parse(tid); From 5f279c87cf7973dfc11a4fcc63521354756a5cc6 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 16:36:33 +0800 Subject: [PATCH 59/81] message type --- lib/im/SocketClient.dart | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 1ea16362..c4d3821e 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -208,8 +208,8 @@ class SocketClient { } } - Future sendMessage(String toId, String content) async { - Map message = createMessage(toId, content, fromId: userId); + Future sendMessage(String toId, String content, {String attach, int msgType, replyId}) async { + Map message = createMessage(toId, content, fromId: userId, attach: attach, msgType: msgType, replyId: replyId); message["state"] = 1; int id = await hxDatabase.insert(message).catchError((error) { debugPrint("insertMessage: $error"); @@ -223,8 +223,19 @@ class SocketClient { return Message.fromJson(message); } message["id"] = id; - Uint8List data = utf8.encode(content); - MsgData msgData = MsgData(to: toId, from: userId, type: MsgType.TEXT, data: data); + + MsgType type = MsgType.values[msgType]; + Uint8List data; + if (type == MsgType.TEXT) { + data = utf8.encode(content); + } else if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { + File file = File(attach); + data = await file.readAsBytes(); + } else { + data = utf8.encode(content); + } + + MsgData msgData = MsgData(to: toId, from: userId, type: type, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); try { _socket.add(proto2.toBytes()); @@ -239,6 +250,8 @@ class SocketClient { return Message.fromJson(message); } + + checkSocket() { if (_socket == null) { reconnect(); From e5c2d32436d1c4638937c8fd03fe809b917e0912 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 17:23:27 +0800 Subject: [PATCH 60/81] message type --- lib/im/SocketClient.dart | 26 ++++--- lib/im/chat_details_page.dart | 131 ++++++++++++++++++++++++---------- 2 files changed, 109 insertions(+), 48 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index c4d3821e..6709938d 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -209,6 +209,21 @@ class SocketClient { } Future sendMessage(String toId, String content, {String attach, int msgType, replyId}) async { + + MsgType type = MsgType.values[msgType]; + Uint8List data; + if (type == MsgType.TEXT) { + data = utf8.encode(content); + } else if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { + File file = File(attach); + Directory dir = await getTemporaryDirectory(); + File newFile = await file.copy("${dir.path}/hx_${DateTime.now().millisecondsSinceEpoch}.${file.path.split(".")[1]}"); + data = await file.readAsBytes(); + attach = newFile.path; + } else { + data = utf8.encode(content); + } + Map message = createMessage(toId, content, fromId: userId, attach: attach, msgType: msgType, replyId: replyId); message["state"] = 1; int id = await hxDatabase.insert(message).catchError((error) { @@ -224,17 +239,6 @@ class SocketClient { } message["id"] = id; - MsgType type = MsgType.values[msgType]; - Uint8List data; - if (type == MsgType.TEXT) { - data = utf8.encode(content); - } else if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { - File file = File(attach); - data = await file.readAsBytes(); - } else { - data = utf8.encode(content); - } - MsgData msgData = MsgData(to: toId, from: userId, type: type, data: data); final proto2 = Proto(5, 1, msgData.writeToBuffer()); try { diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 66a9486a..51822d5b 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -312,6 +312,12 @@ class _ChatDetailsPage extends State } else { dynamicType = 2; } + + List filePath = mediaPaths.map((e) => e.path).toList(); + filePath.forEach((path) { + socketClient.sendMessage(_toUser.mid, "", attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4); + }); + } setState(() {}); } @@ -428,6 +434,7 @@ class _ChatDetailsPage extends State Widget chatDetailsItem(position) { bool isSelf = messages[position].fromId == selfUserId; bool isText = messages[position].msgType == 1; + bool isImage = messages[position].msgType == 2; return Container( padding: EdgeInsets.only( top: 16.h, @@ -570,11 +577,13 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Navigator.of(context).pushNamed('/router/personal_page', - arguments: { - "memberId": _toUser?.mid ?? "", - "inletType": 1 - }); + Navigator.of(context).pushNamed( + '/router/personal_page', + arguments: { + "memberId": _toUser?.mid ?? "", + "inletType": 1 + }, + ); }, child: MImage( _toUser.avatar, @@ -584,7 +593,8 @@ class _ChatDetailsPage extends State fit: BoxFit.cover, errorSrc: "assets/image/default_1.webp", fadeSrc: "assets/image/default_1.webp", - )), + ), + ), SizedBox( width: 12.w, ), @@ -807,30 +817,26 @@ class _ChatDetailsPage extends State fit: BoxFit.cover, errorSrc: "assets/image/default_1.webp", fadeSrc: "assets/image/default_1.webp", - )), + ), + ), ], ), ), /// not self image 图片 - if (!isSelf && !isText) + if (!isSelf && isImage) Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w, top: 20.h), child: Row( children: [ - // MImage( - // "", - // isCircle: true, - // width: 44, - // height: 44, - // fit: BoxFit.cover, - // errorSrc: "assets/image/default_1.webp", - // fadeSrc: "assets/image/default_1.webp", - // ), - Image.asset( - "assets/image/fuka_zj.webp", - height: 44.h, - width: 44.h, + MImage( + userInfo?.headimg ?? "", + isCircle: true, + width: 44, + height: 44, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", ), SizedBox( width: 12.w, @@ -847,24 +853,74 @@ class _ChatDetailsPage extends State offset: Offset(0, 4), blurRadius: 4, spreadRadius: 0, - ) + ), ], ), child: GestureDetector( onLongPress: () { setState(() {}); }, - child: Image.asset( - "assets/image/icon_guide_4.webp", + child: Image.file( + File(messages[position].attach), width: 180.h, height: 200.h, fit: BoxFit.fill, ), - ))), + ), + ), + ), + Spacer(), + ], + ), + ), + + /// self image 图片 + if (isSelf && isImage) + Padding( + padding: EdgeInsets.only(left: 39.w, right: 17.w, top: 20.h), + child: Row( + children: [ + Spacer(), Expanded( - flex: 1, - child: Container(), - ) + flex: 3, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: GestureDetector( + onLongPress: () { + setState(() {}); + }, + child: Image.file( + File(messages[position].attach), + width: 180.h, + height: 200.h, + fit: BoxFit.fill, + ), + ), + ), + ), + SizedBox( + width: 12.w, + ), + MImage( + userInfo?.headimg ?? "", + isCircle: true, + width: 44, + height: 44, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + ), ], ), ), @@ -922,8 +978,7 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient - .sendMessage(_toUser.mid, commentText) + socketClient.sendMessage(_toUser.mid, commentText) .then((value) { Message message = value; messages.insert(0, message); @@ -962,14 +1017,15 @@ class _ChatDetailsPage extends State "assets/image/icon_chat_emo.webp", height: 26.h, width: 26.h, - )), + ), + ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - // _onMoreTap(); - // jumpToBottom(); - SmartDialog.showToast("暂不支持", alignment: Alignment.center); + _onMoreTap(); + jumpToBottom(); + // SmartDialog.showToast("暂不支持", alignment: Alignment.center); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), @@ -1123,13 +1179,14 @@ class _ChatDetailsPage extends State ), ), ], - )), + ), + ), ], - ) + ), ], ), ), - ) + ), ], ), ); From 741852b0db248d01719727f19c1afe9d171d623a Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 27 Sep 2024 17:32:49 +0800 Subject: [PATCH 61/81] =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BB=BF=E5=BE=AE=E4=BF=A1=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=EF=BC=9B=20=E5=88=97=E8=A1=A8=E6=96=B0=E5=A2=9E=E5=B8=B8?= =?UTF-8?q?=E8=A7=84=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/image/2x/icon_gz.webp | Bin 1400 -> 3368 bytes assets/image/2x/icon_pl.webp | Bin 1296 -> 3351 bytes assets/image/2x/icon_system_message_new.webp | Bin 0 -> 3809 bytes assets/image/2x/icon_z.webp | Bin 1352 -> 2268 bytes assets/image/3x/icon_gz.webp | Bin 2436 -> 5665 bytes assets/image/3x/icon_pl.webp | Bin 2162 -> 5680 bytes assets/image/3x/icon_system_message_new.webp | Bin 0 -> 6450 bytes assets/image/3x/icon_z.webp | Bin 2002 -> 3634 bytes assets/image/icon_gz.webp | Bin 822 -> 1464 bytes assets/image/icon_pl.webp | Bin 762 -> 1548 bytes assets/image/icon_system_message_new.webp | Bin 0 -> 1757 bytes assets/image/icon_z.webp | Bin 806 -> 1085 bytes lib/im/im_view/im_page.dart | 309 +++++++++++++++---- lib/message/system_message.dart | 2 +- 14 files changed, 250 insertions(+), 61 deletions(-) create mode 100644 assets/image/2x/icon_system_message_new.webp create mode 100644 assets/image/3x/icon_system_message_new.webp create mode 100644 assets/image/icon_system_message_new.webp diff --git a/assets/image/2x/icon_gz.webp b/assets/image/2x/icon_gz.webp index e2069d8430ff94fd20af0c1948da100edbddf9e6..f3e96b018e5907d906addafd6d4d7b5ea3d63fd0 100644 GIT binary patch literal 3368 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 G3;+OaAG3`B diff --git a/assets/image/2x/icon_pl.webp b/assets/image/2x/icon_pl.webp index 6d38ebfdf8ac695dc489ee609f5b2e4abc3dd84b..6fbd81f2b9ae8ecb4cd438eccc3d3d5d87c84f72 100644 GIT binary patch literal 3351 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 GzyJV^FL#Cj diff --git a/assets/image/2x/icon_system_message_new.webp b/assets/image/2x/icon_system_message_new.webp new file mode 100644 index 0000000000000000000000000000000000000000..e024425e4302eaa601fbeba27ab7ed62272b2da2 GIT binary patch literal 3809 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 XGP&sS&+Vh(00000NkvXXu0mjf-|A4h literal 0 HcmV?d00001 diff --git a/assets/image/2x/icon_z.webp b/assets/image/2x/icon_z.webp index 65835cb42c4e429bfb4379da214848ba9784c600..d28140653e3909ff73487436778beeed4ae17ecd 100644 GIT binary patch literal 2268 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 T!m}3(&;I;B|GmJ$T6IPMLh;~5 diff --git a/assets/image/3x/icon_gz.webp b/assets/image/3x/icon_gz.webp index 00e33235e449fd0f114eed9ac7b21181a463072a..41f1ad5717f84f70ed42bcd3438aae71a3b79d47 100644 GIT binary patch literal 5665 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 CcEq>< diff --git a/assets/image/3x/icon_pl.webp b/assets/image/3x/icon_pl.webp index dc69394a40a5cc0e89692c073bfd3bbaa2f8d95e..96db2bea9ed73634dd203f27f7a929d777b5a86e 100644 GIT binary patch literal 5680 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 diff --git a/assets/image/3x/icon_system_message_new.webp b/assets/image/3x/icon_system_message_new.webp new file mode 100644 index 0000000000000000000000000000000000000000..96ab6a45778aabcf72268633ed92f0e13bf54e71 GIT binary patch literal 6450 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 DX)i9n literal 0 HcmV?d00001 diff --git a/assets/image/3x/icon_z.webp b/assets/image/3x/icon_z.webp index 953f1d79b6707385e7c35cba021e8e50f98f023b..ab5f6f2e61cd90ceb9f802a1a39cf7e8b52e2603 100644 GIT binary patch literal 3634 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 Sx);g-0000A literal 822 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 diff --git a/assets/image/icon_system_message_new.webp b/assets/image/icon_system_message_new.webp new file mode 100644 index 0000000000000000000000000000000000000000..f22dd2631b1492df2b5905483e68f2977bb685e2 GIT binary patch literal 1757 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 Dcn|y0 literal 806 zcmWIYbaRtqW?%?+bqWXzu!!JdU|=u+Vs$X`baoCn!3g9sFrEOBEI_H;ypp0IcPE92 z$S4Jn7z`vP7Zxx$&AP0MMV^olXG&^uGEmV6AXX^?S`KuR2#_t34qYF0&|qF4&TdT=2G2<{u0dwye8w;T^UF>Oin4J35}$Z`Td&~aYV~*gU_Y4L zb(!?!iYbUK@U&`@%E|M~;?DA_NT#x>UZ1pNQl@j?lC0zdGq2?K&DgZV^x}#$)8_P? znf)*B`sOcnLT9f9ekv2{-6j;{zJ&8^uM@AY`X`CUFP~qu`<9lL_Ra9izLIZ`Kc$sD zI(X)MMt{bC&N9iCmF5Pm-KX_hTYvBK<=VGjp?U4=3+wFd@7>t3Qr}blVVGb+(jL!U hps{)qkr&fh7t52c8-R_nY-B3;@si*E;|J diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index f775ae7c..53b2611e 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -1,10 +1,11 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/retrofit/data/msg_stats.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; @@ -16,6 +17,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../retrofit/data/im_user.dart'; +import '../../retrofit/data/page.dart'; import '../../utils/flutter_utils.dart'; import '../../view_widget/custom_image.dart'; import 'on_chat_message.dart'; @@ -49,6 +51,7 @@ class _IMPage extends State implements OnChatMessage { Map contactMap = {}; int insertIndex = 0; String selfUserId; + final RefreshController _refreshController = RefreshController(); @override void onMessage(txt) { @@ -72,7 +75,7 @@ class _IMPage extends State implements OnChatMessage { SharedPreferences.getInstance().then((value) { apiService = ApiService(Dio(), token: value.getString("token"), context: context); - // queryMsgStats(); + queryMsgStats(); }); } @@ -96,7 +99,7 @@ class _IMPage extends State implements OnChatMessage { _refresh() { pageNum = 1; loadMessageList(); - // queryMsgStats(); + queryMsgStats(); } listenerRefresh(Message message) async { @@ -218,7 +221,34 @@ class _IMPage extends State implements OnChatMessage { } } - final RefreshController _refreshController = RefreshController(); + ///App消息 统计各类消息数量 + queryMsgStats() async { + if (apiService == null) { + SharedPreferences value = await SharedPreferences.getInstance(); + apiService = ApiService( + Dio(), + context: context, + token: value.getString("token"), + ); + } + BaseData> baseData = + await apiService.stats().catchError((onError) {}); + if (baseData != null && baseData.isSuccess) { + setState(() { + msgNumber.forEach((key, value) { + msgNumber[key] = 0; + }); + baseData.data.forEach((element) { + if (msgNumber.containsKey(element.name)) { + msgNumber[element.name] = element.number; + } + }); + }); + _refreshController.loadComplete(); + _refreshController.refreshCompleted(); + } + EasyLoading.dismiss(); + } @override Widget build(BuildContext context) { @@ -305,7 +335,56 @@ class _IMPage extends State implements OnChatMessage { ), ), imPageSearch(), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: (){ + Navigator.of(context).pushNamed('/router/system_notice').then((value) { + setState(() { + msgNumber["2"] = 0; + msgNumber["3"] = 0; + }); + }); + }, + child:messageItem("assets/image/icon_system_message_new.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: (){ + Navigator.of(context).pushNamed('/router/system_details', + arguments: {"msgType": 4}).then((value) { + setState(() { + msgNumber["4"] = 0; + }); + }); + }, + child:messageItem("assets/image/icon_gz.webp", S.of(context).guanzhu,msgNumber["4"].toString()), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: (){ + Navigator.of(context).pushNamed('/router/system_details', + arguments: {"msgType": 6}).then((value) { + setState(() { + msgNumber["6"] = 0; + }); + }); + }, + child:messageItem("assets/image/icon_pl.webp", S.of(context).pinglun, msgNumber["6"].toString()), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: (){ + Navigator.of(context).pushNamed('/router/system_details', + arguments: {"msgType": 5}).then((value) { + setState(() { + msgNumber["5"] = 0; + }); + }); + }, + child: messageItem("assets/image/icon_z.webp", S.of(context).dianzan, msgNumber["5"].toString()), + ), chatList(), + SizedBox(height:100.h) ], ), ), @@ -324,7 +403,7 @@ class _IMPage extends State implements OnChatMessage { }); }, child: Container( - margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), + margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h), padding: EdgeInsets.symmetric(vertical: 13.h), decoration: BoxDecoration( color: Color(0xFFFDFCFC), @@ -355,67 +434,116 @@ class _IMPage extends State implements OnChatMessage { ); } + bool _isDelEnter = false; + Map idsController = {}; + String lastScrollId; + double lastScrollOffset = 0; + bool _isScrollOpen = false; + ///聊天列表 Widget chatList() { - return Container( - child: SlidableAutoCloseBehavior( - child: ListView( - padding: EdgeInsets.only(top: 16), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - children: conversationIds.map((e) { - int position = conversationIds.indexOf(e); - return ClipRRect( - // borderRadius: BorderRadius.all(Radius.circular(4)), - child: Slidable( - groupTag: true, - endActionPane: ActionPane( - extentRatio:0.16, - motion: ScrollMotion(), - children: [ - CustomSlidableAction( - onPressed: (bc) { - showDelDialog(conversationIds[position]); - }, - backgroundColor: Color(0xFFFB312B), - foregroundColor: Colors.white, - child: Container( - color: Colors.red, - height: double.infinity, - alignment: Alignment.center, - child: Text( - S.of(context).shanchu, - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ), + return ListView( + padding: EdgeInsets.only(top:8.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + children: conversationIds.map((e) { + ScrollController scrollController; + if (idsController.containsKey(e)) + scrollController = idsController[e]; + else { + scrollController = ScrollController(); + idsController[e] = scrollController; + } + scrollController.addListener(() { + if (scrollController.offset > 0) { + if (lastScrollId != null && lastScrollId != e) + idsController[lastScrollId].jumpTo(0); + if(lastScrollOffset < scrollController.offset){ + scrollController.jumpTo(scrollController.position.maxScrollExtent); + _isScrollOpen = true; + }else if(lastScrollOffset > scrollController.offset && _isScrollOpen){ + scrollController.jumpTo(0); + } + lastScrollId = e; + // scrollController.animateTo( + // lastScrollOffset == scrollController.position.maxScrollExtent + // ? 0 + // : scrollController.position.maxScrollExtent, + // duration: Duration(milliseconds: 100), + // curve: Curves.ease); + lastScrollOffset = scrollController.offset; + } else { + if (lastScrollId == e) { + setState(() { + _isDelEnter = false; + _isScrollOpen = false; + }); + } + } + }); + int position = conversationIds.indexOf(e); + return SingleChildScrollView( + physics: ClampingScrollPhysics(), + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/chat_details_page', + arguments: { + "toUser": contactMap[conversationIds[position]], + }, + ).then((value) { + unreadCountMap[conversationIds[position]] = 0; + updateLastMessage(conversationIds[position]); + _refresh(); + }); + }, + child: chatItem(conversationIds[position]), + ), + GestureDetector( + child: Container( + color: Colors.red, + alignment: Alignment.center, + padding: + EdgeInsets.symmetric(vertical: 25.h, horizontal: 14.w), + child: Text( + _isDelEnter && lastScrollId == e + ? "删除并清空" + : S.of(context).shanchu, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: MyFontWeight.regular, ), - ], + ), ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).pushNamed( - '/router/chat_details_page', - arguments: { - "toUser": contactMap[conversationIds[position]], - }, - ).then((value) { - unreadCountMap[conversationIds[position]] = 0; - updateLastMessage(conversationIds[position]); - _refresh(); + onTap: () async{ + // showDelDialog(conversationIds[position]); + if (_isDelEnter) { + await hxDatabase.deleteByUser(conversationIds[position]); + lastScrollId = null; + _refresh(); + } else { + Future.delayed(Duration(milliseconds: 100), () { + idsController[lastScrollId].animateTo( + idsController[lastScrollId].position.maxScrollExtent, + duration: Duration(milliseconds: 100), + curve: Curves.ease); }); - }, - child: chatItem(conversationIds[position]), - ), + } + setState(() { + _isDelEnter = !_isDelEnter; + }); + }, ), - ); - }).toList(), - ), - ), + ], + ), + ); + }).toList(), ); } @@ -424,6 +552,7 @@ class _IMPage extends State implements OnChatMessage { padding: EdgeInsets.only( left: 16.w, right: 17.w, bottom: 18.h, ), + width: MediaQuery.of(context).size.width, child: Row( children: [ MImage( @@ -518,6 +647,66 @@ class _IMPage extends State implements OnChatMessage { ); } + Widget messageItem(img, title, messageNum) { + return Container( + padding: EdgeInsets.only(top:8.h,bottom:8.h, left:16.w,right:15.w), + child: Column( + children: [ + Row( + children: [ + Image.asset( + img, + fit: BoxFit.fill, + ), + SizedBox( + width: 12.w, + ), + Text( + title, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF060606), + fontWeight: MyFontWeight.semi_bold, + ), + ), + Spacer(), + if(messageNum != "0") + ((double.tryParse(messageNum) < 100)? + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + child: RoundButton( + text:messageNum, + textColor: Colors.white, + fontWeight: MyFontWeight.regular, + backgroup: Color(0xFFFF441A), + fontSize: 10.sp, + radius: 100, + )): + Container( + padding: EdgeInsets.symmetric(horizontal:4.w,vertical:2.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + child: RoundButton( + text:"99+", + textColor: Colors.white, + fontWeight: MyFontWeight.regular, + backgroup: Color(0xFFFF441A), + fontSize: 10.sp, + radius: 100, + ))), + ], + ), + ], + ), + ); + } ///确认删除弹窗 showDelDialog(conversationId) { diff --git a/lib/message/system_message.dart b/lib/message/system_message.dart index 63fd82fe..de0990ac 100644 --- a/lib/message/system_message.dart +++ b/lib/message/system_message.dart @@ -287,7 +287,7 @@ class _SystemMessagePage extends State{ }); }); }, - child:messageItem("assets/image/icon_system_message.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()), + child:messageItem("assets/image/icon_system_message_new.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()), ), // newsSurvey(), // SizedBox( From 720f7ca7d5f35882f8feb94efa1692025f782faa Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 27 Sep 2024 17:35:06 +0800 Subject: [PATCH 62/81] =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B8=B8=E8=A7=84=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 53b2611e..c7a32ea3 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -483,7 +483,7 @@ class _IMPage extends State implements OnChatMessage { }); int position = conversationIds.indexOf(e); return SingleChildScrollView( - physics: ClampingScrollPhysics(), + physics: BouncingScrollPhysics(), controller: scrollController, scrollDirection: Axis.horizontal, child: Row( From 48d50fe836090aaf6ee34021b626f1e608328b10 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 17:40:29 +0800 Subject: [PATCH 63/81] message type --- lib/im/SocketClient.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 6709938d..51eba209 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -208,7 +208,7 @@ class SocketClient { } } - Future sendMessage(String toId, String content, {String attach, int msgType, replyId}) async { + Future sendMessage(String toId, String content, {String attach, int msgType = 1, replyId}) async { MsgType type = MsgType.values[msgType]; Uint8List data; From e08b98d18eb19e895030e5af7774cd37669ab914 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Fri, 27 Sep 2024 18:06:43 +0800 Subject: [PATCH 64/81] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=9B=B4=E6=94=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 2 +- lib/im/chat_details_page.dart | 64 ++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 6709938d..51eba209 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -208,7 +208,7 @@ class SocketClient { } } - Future sendMessage(String toId, String content, {String attach, int msgType, replyId}) async { + Future sendMessage(String toId, String content, {String attach, int msgType = 1, replyId}) async { MsgType type = MsgType.values[msgType]; Uint8List data; diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 51822d5b..833ec6e5 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -24,6 +24,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; +import '../community/photo_view_gallery_screen.dart'; import '../retrofit/data/base_data.dart'; import '../retrofit/data/user_info.dart'; import '../utils/flutter_utils.dart'; @@ -842,20 +843,20 @@ class _ChatDetailsPage extends State width: 12.w, ), Expanded( - flex: 3, + // flex: 3, child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(6), + // color: Color(0xFFFFFFFF), + // boxShadow: [ + // BoxShadow( + // color: Color(0xFFA8A3A3).withAlpha(12), + // offset: Offset(0, 4), + // blurRadius: 4, + // spreadRadius: 0, + // ), + // ], + // ), child: GestureDetector( onLongPress: () { setState(() {}); @@ -864,7 +865,7 @@ class _ChatDetailsPage extends State File(messages[position].attach), width: 180.h, height: 200.h, - fit: BoxFit.fill, + fit: BoxFit.contain, ), ), ), @@ -882,29 +883,30 @@ class _ChatDetailsPage extends State children: [ Spacer(), Expanded( - flex: 3, + // flex: 3, child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(6), + // color: Color(0xFFFFFFFF), + // boxShadow: [ + // BoxShadow( + // color: Color(0xFFA8A3A3).withAlpha(12), + // offset: Offset(0, 4), + // blurRadius: 4, + // spreadRadius: 0, + // ), + // ], + // ), child: GestureDetector( onLongPress: () { setState(() {}); }, + onTap:(){}, child: Image.file( File(messages[position].attach), width: 180.h, height: 200.h, - fit: BoxFit.fill, + fit: BoxFit.contain, ), ), ), @@ -1023,9 +1025,9 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - _onMoreTap(); - jumpToBottom(); - // SmartDialog.showToast("暂不支持", alignment: Alignment.center); + // _onMoreTap(); + // jumpToBottom(); + SmartDialog.showToast("暂不支持", alignment: Alignment.center); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), From 5441937532d012259a31b8c1f15ec9fb7219f413 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 18:12:25 +0800 Subject: [PATCH 65/81] change convasion refresh range --- lib/im/SocketClient.dart | 3 - lib/im/chat_details_page.dart | 19 +- lib/im/im_view/im_page.dart | 425 +++++++++++++++------------- lib/view_widget/classic_header.dart | 1 + 4 files changed, 244 insertions(+), 204 deletions(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 51eba209..01f7f05f 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -209,7 +209,6 @@ class SocketClient { } Future sendMessage(String toId, String content, {String attach, int msgType = 1, replyId}) async { - MsgType type = MsgType.values[msgType]; Uint8List data; if (type == MsgType.TEXT) { @@ -254,8 +253,6 @@ class SocketClient { return Message.fromJson(message); } - - checkSocket() { if (_socket == null) { reconnect(); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 51822d5b..b406082a 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -422,8 +422,14 @@ class _ChatDetailsPage extends State onTap: () { setState(() { copyIndex = 0; - FocusScope.of(context).requestFocus(FocusNode()); + if (emojiShowing) { + emojiShowing = !emojiShowing; + } + if (moreShow) { + moreShow = !moreShow; + } }); + FocusScope.of(context).requestFocus(FocusNode()); }, child: chatDetailsItem(position), ); @@ -573,6 +579,7 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( behavior: HitTestBehavior.opaque, @@ -735,6 +742,7 @@ class _ChatDetailsPage extends State padding: EdgeInsets.only(left: 36.w, right: 16.w), child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ if (messages[position].state == 3) Container( @@ -806,8 +814,13 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Navigator.of(context).pushNamed('/router/personal_page', - arguments: {"memberId": "0", "inletType": 1}); + Navigator.of(context).pushNamed( + '/router/personal_page', + arguments: { + "memberId": "0", + "inletType": 1, + }, + ); }, child: MImage( userInfo?.headimg ?? "", diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index c7a32ea3..0fb970b7 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -74,7 +74,8 @@ class _IMPage extends State implements OnChatMessage { initSocketClient(); SharedPreferences.getInstance().then((value) { - apiService = ApiService(Dio(), token: value.getString("token"), context: context); + apiService = + ApiService(Dio(), token: value.getString("token"), context: context); queryMsgStats(); }); } @@ -118,9 +119,12 @@ class _IMPage extends State implements OnChatMessage { loadMessageList() async { messages = await hxDatabase.queryList(); - lastMessageMap = messages.lGroupBy((p0) => p0.conversationId).mGroupItem(key: (p1) => num.parse(p1.time)); + lastMessageMap = messages + .lGroupBy((p0) => p0.conversationId) + .mGroupItem(key: (p1) => num.parse(p1.time)); - await queryImUserInfo(messages.map((e) => e.toId != selfUserId ? e.toId : e.fromId).toList()); + await queryImUserInfo( + messages.map((e) => e.toId != selfUserId ? e.toId : e.fromId).toList()); await sortConversation(lastMessageMap); @@ -132,11 +136,10 @@ class _IMPage extends State implements OnChatMessage { /// update conversation by time sort sortConversation(lastMessageMap) async { List sortMessages = lastMessageMap.values.toList(); - sortMessages.sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time))); - conversationIds = sortMessages - .map((e) => e.conversationId) - .toSet() - .toList(); + sortMessages + .sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time))); + conversationIds = + sortMessages.map((e) => e.conversationId).toSet().toList(); } /// update conversation unreadcount @@ -152,16 +155,20 @@ class _IMPage extends State implements OnChatMessage { await queryMemberInfo(userIds); return; } else { - List queryUserIds = userIds.where((u) => contacts.where((c) => c.mid == u).isEmpty).toList(); + List queryUserIds = userIds + .where((u) => contacts.where((c) => c.mid == u).isEmpty) + .toList(); if (queryUserIds.isNotEmpty) { await queryMemberInfo(queryUserIds); return; } } - contactMap = contacts.lGroupBy((p0) => conversationId(p0.mid, selfUserId)).mGroupItem(); - List topConversationIds = [],notTopUserIds = []; + contactMap = contacts + .lGroupBy((p0) => conversationId(p0.mid, selfUserId)) + .mGroupItem(); + List topConversationIds = [], notTopUserIds = []; contactMap.forEach((key, value) { - if(value.isTop == 1) + if (value.isTop == 1) topConversationIds.add(key); else notTopUserIds.add(key); @@ -173,7 +180,8 @@ class _IMPage extends State implements OnChatMessage { /// update one conversation last message ,and update conversation sort void updateLastMessage(String conversationId) async { Message message = await hxDatabase.lastMessage(conversationId); - debugPrint("lastmessage: $conversationId ${message?.content} ${message?.toJson()}"); + debugPrint( + "lastmessage: $conversationId ${message?.content} ${message?.toJson()}"); if (message != null) { lastMessageMap[conversationId] = message; await sortConversation(lastMessageMap); @@ -187,7 +195,7 @@ class _IMPage extends State implements OnChatMessage { } refreshState() { - if(_refreshController.isRefresh)_refreshController.refreshCompleted(); + if (_refreshController.isRefresh) _refreshController.refreshCompleted(); if (mounted) setState(() {}); } @@ -232,7 +240,7 @@ class _IMPage extends State implements OnChatMessage { ); } BaseData> baseData = - await apiService.stats().catchError((onError) {}); + await apiService.stats().catchError((onError) {}); if (baseData != null && baseData.isSuccess) { setState(() { msgNumber.forEach((key, value) { @@ -268,126 +276,146 @@ class _IMPage extends State implements OnChatMessage { stops: [0, 0.2, 0.4, 1], ), ), - child: SmartRefresher( - enablePullDown: true, - enablePullUp: false, - header: MyHeader(), - physics: BouncingScrollPhysics(), - footer: CustomFooter( - loadStyle: LoadStyle.ShowWhenLoading, - builder: (BuildContext context, LoadStatus mode) { - return (messages.length == 0) ? Container() : MyFooter(mode); - }, - ), - controller: _refreshController, - onRefresh: _refresh, - onLoading: () { - _refresh(); - }, - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - child: Column( - children: [ - Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 12.h, - bottom: 15.h, - right: 16.w, - left: 16.w, + child: Column( + children: [ + Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 12.h, + bottom: 15.h, + right: 16.w, + left: 16.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + S.of(context).xiaoxi, + style: TextStyle( + color: Colors.black, + fontSize: 18.sp, + fontWeight: MyFontWeight.bold, + ), + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - S.of(context).xiaoxi, - style: TextStyle( - color: Colors.black, - fontSize: 18.sp, - fontWeight: MyFontWeight.bold, - ), - ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context) + .pushNamed('/router/chat_friend_group') + .then((value) { + _refresh(); + }); + }, + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(20.r), ), + child: Image.asset( + "assets/image/friend_grouping.webp", + fit: BoxFit.fill, + height: 14.h, + width: 14.h, + ), + ), + ), + ], + ), + ), + imPageSearch(), + Expanded( + child: SmartRefresher( + enablePullDown: true, + enablePullUp: false, + header: MyHeader(), + physics: BouncingScrollPhysics(), + footer: CustomFooter( + loadStyle: LoadStyle.ShowWhenLoading, + builder: (BuildContext context, LoadStatus mode) { + return (messages.length == 0) + ? Container() + : MyFooter(mode); + }, + ), + controller: _refreshController, + onRefresh: _refresh, + onLoading: () { + _refresh(); + }, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Navigator.of(context).pushNamed('/router/chat_friend_group') + Navigator.of(context) + .pushNamed('/router/system_notice') .then((value) { - _refresh(); + setState(() { + msgNumber["2"] = 0; + msgNumber["3"] = 0; + }); }); }, - child: Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: Color(0xFFFFFFFF), - borderRadius: BorderRadius.circular(20.r), - ), - child: Image.asset( - "assets/image/friend_grouping.webp", - fit: BoxFit.fill, - height: 14.h, - width: 14.h, - ), - ), + child: messageItem( + "assets/image/icon_system_message_new.webp", + S.of(context).xitongxiaoxi, + (msgNumber["2"] + msgNumber["3"]).toString()), ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/system_details', + arguments: {"msgType": 4}).then((value) { + setState(() { + msgNumber["4"] = 0; + }); + }); + }, + child: messageItem("assets/image/icon_gz.webp", + S.of(context).guanzhu, msgNumber["4"].toString()), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/system_details', + arguments: {"msgType": 6}).then((value) { + setState(() { + msgNumber["6"] = 0; + }); + }); + }, + child: messageItem("assets/image/icon_pl.webp", + S.of(context).pinglun, msgNumber["6"].toString()), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/system_details', + arguments: {"msgType": 5}).then((value) { + setState(() { + msgNumber["5"] = 0; + }); + }); + }, + child: messageItem("assets/image/icon_z.webp", + S.of(context).dianzan, msgNumber["5"].toString()), + ), + chatList(), + SizedBox(height: 100.h) ], ), ), - imPageSearch(), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: (){ - Navigator.of(context).pushNamed('/router/system_notice').then((value) { - setState(() { - msgNumber["2"] = 0; - msgNumber["3"] = 0; - }); - }); - }, - child:messageItem("assets/image/icon_system_message_new.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: (){ - Navigator.of(context).pushNamed('/router/system_details', - arguments: {"msgType": 4}).then((value) { - setState(() { - msgNumber["4"] = 0; - }); - }); - }, - child:messageItem("assets/image/icon_gz.webp", S.of(context).guanzhu,msgNumber["4"].toString()), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: (){ - Navigator.of(context).pushNamed('/router/system_details', - arguments: {"msgType": 6}).then((value) { - setState(() { - msgNumber["6"] = 0; - }); - }); - }, - child:messageItem("assets/image/icon_pl.webp", S.of(context).pinglun, msgNumber["6"].toString()), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: (){ - Navigator.of(context).pushNamed('/router/system_details', - arguments: {"msgType": 5}).then((value) { - setState(() { - msgNumber["5"] = 0; - }); - }); - }, - child: messageItem("assets/image/icon_z.webp", S.of(context).dianzan, msgNumber["5"].toString()), - ), - chatList(), - SizedBox(height:100.h) - ], + ), ), - ), + ], ), ), ); @@ -397,7 +425,7 @@ class _IMPage extends State implements OnChatMessage { Widget imPageSearch() { return GestureDetector( behavior: HitTestBehavior.opaque, - onTap:(){ + onTap: () { Navigator.of(context).pushNamed('/router/im_search').then((value) { _refresh(); }); @@ -429,8 +457,7 @@ class _IMPage extends State implements OnChatMessage { ), ), ], - ) - ), + )), ); } @@ -443,7 +470,7 @@ class _IMPage extends State implements OnChatMessage { ///聊天列表 Widget chatList() { return ListView( - padding: EdgeInsets.only(top:8.h), + padding: EdgeInsets.only(top: 8.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), children: conversationIds.map((e) { @@ -458,10 +485,12 @@ class _IMPage extends State implements OnChatMessage { if (scrollController.offset > 0) { if (lastScrollId != null && lastScrollId != e) idsController[lastScrollId].jumpTo(0); - if(lastScrollOffset < scrollController.offset){ - scrollController.jumpTo(scrollController.position.maxScrollExtent); + if (lastScrollOffset < scrollController.offset) { + scrollController + .jumpTo(scrollController.position.maxScrollExtent); _isScrollOpen = true; - }else if(lastScrollOffset > scrollController.offset && _isScrollOpen){ + } else if (lastScrollOffset > scrollController.offset && + _isScrollOpen) { scrollController.jumpTo(0); } lastScrollId = e; @@ -509,7 +538,7 @@ class _IMPage extends State implements OnChatMessage { color: Colors.red, alignment: Alignment.center, padding: - EdgeInsets.symmetric(vertical: 25.h, horizontal: 14.w), + EdgeInsets.symmetric(vertical: 25.h, horizontal: 14.w), child: Text( _isDelEnter && lastScrollId == e ? "删除并清空" @@ -521,7 +550,7 @@ class _IMPage extends State implements OnChatMessage { ), ), ), - onTap: () async{ + onTap: () async { // showDelDialog(conversationIds[position]); if (_isDelEnter) { await hxDatabase.deleteByUser(conversationIds[position]); @@ -550,7 +579,9 @@ class _IMPage extends State implements OnChatMessage { Widget chatItem(conversationId) { return Container( padding: EdgeInsets.only( - left: 16.w, right: 17.w, bottom: 18.h, + left: 16.w, + right: 17.w, + bottom: 18.h, ), width: MediaQuery.of(context).size.width, child: Row( @@ -620,7 +651,8 @@ class _IMPage extends State implements OnChatMessage { ), ), ), - if (unreadCountMap[conversationId] != null && unreadCountMap[conversationId] > 0) + if (unreadCountMap[conversationId] != null && + unreadCountMap[conversationId] > 0) Container( width: 16, height: 16, @@ -649,7 +681,7 @@ class _IMPage extends State implements OnChatMessage { Widget messageItem(img, title, messageNum) { return Container( - padding: EdgeInsets.only(top:8.h,bottom:8.h, left:16.w,right:15.w), + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 16.w, right: 15.w), child: Column( children: [ Row( @@ -670,37 +702,38 @@ class _IMPage extends State implements OnChatMessage { ), ), Spacer(), - if(messageNum != "0") - ((double.tryParse(messageNum) < 100)? - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Color(0xFFFF441A), - ), - child: RoundButton( - text:messageNum, - textColor: Colors.white, - fontWeight: MyFontWeight.regular, - backgroup: Color(0xFFFF441A), - fontSize: 10.sp, - radius: 100, - )): - Container( - padding: EdgeInsets.symmetric(horizontal:4.w,vertical:2.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Color(0xFFFF441A), - ), - child: RoundButton( - text:"99+", - textColor: Colors.white, - fontWeight: MyFontWeight.regular, - backgroup: Color(0xFFFF441A), - fontSize: 10.sp, - radius: 100, - ))), + if (messageNum != "0") + ((double.tryParse(messageNum) < 100) + ? Container( + width: 16, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + child: RoundButton( + text: messageNum, + textColor: Colors.white, + fontWeight: MyFontWeight.regular, + backgroup: Color(0xFFFF441A), + fontSize: 10.sp, + radius: 100, + )) + : Container( + padding: EdgeInsets.symmetric( + horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Color(0xFFFF441A), + ), + child: RoundButton( + text: "99+", + textColor: Colors.white, + fontWeight: MyFontWeight.regular, + backgroup: Color(0xFFFF441A), + fontSize: 10.sp, + radius: 100, + ))), ], ), ], @@ -725,23 +758,24 @@ class _IMPage extends State implements OnChatMessage { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child:Container( - alignment:Alignment.center, - child: Text( - "删除并清空聊天记录", - style: TextStyle( - color: Color(0xFF060606), - fontSize: 16.sp, - fontWeight: MyFontWeight.bold, + Expanded( + child: Container( + alignment: Alignment.center, + child: Text( + "删除并清空聊天记录", + style: TextStyle( + color: Color(0xFF060606), + fontSize: 16.sp, + fontWeight: MyFontWeight.bold, + ), ), ), ), - ), // Spacer(), Container( - height:1.h, + height: 1.h, width: double.infinity, - color:Color(0xFFEDEDED), + color: Color(0xFFEDEDED), ), Row( children: [ @@ -752,38 +786,33 @@ class _IMPage extends State implements OnChatMessage { Navigator.of(context).pop(); }, child: Container( - child: Text( - "取消", + child: Text("取消", textAlign: TextAlign.center, style: TextStyle( fontSize: 16.sp, color: Color(0xFF060606), - ) - ) - ) - ) - ), + ))))), Container( - height:45, + height: 45, width: 1.w, color: Color(0xFFEDEDED), ), Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async{ - await hxDatabase.deleteByUser(conversationId); - _refresh(); - Navigator.of(context).pop(); - }, - child: Text( - "确认", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFFF370A), - ), - ), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + await hxDatabase.deleteByUser(conversationId); + _refresh(); + Navigator.of(context).pop(); + }, + child: Text( + "确认", + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFFF370A), + ), ), + ), ), ], ) diff --git a/lib/view_widget/classic_header.dart b/lib/view_widget/classic_header.dart index f61494e4..1345761d 100644 --- a/lib/view_widget/classic_header.dart +++ b/lib/view_widget/classic_header.dart @@ -17,6 +17,7 @@ class MyHeader extends StatelessWidget { @override Widget build(BuildContext context) { return MyClassicHeader( + height: 80, refreshStyle: RefreshStyle.Follow, completeText: S.of(context).shuaxinchenggong, failedText: S.of(context).shuaxinshibai, From e912ab1fb44932776a554297a2492e9e16c5acfa Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 27 Sep 2024 19:30:36 +0800 Subject: [PATCH 66/81] message list image async resize --- lib/im/chat_details_page.dart | 135 ++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 7f488884..0e01fae5 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:ui'; @@ -107,6 +108,7 @@ class _ChatDetailsPage extends State Future refresh() async { List messagePage = await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); + debugPrint("refresh-message-list: ${messagePage.length} page: $page"); if (messagePage.isEmpty) { refreshController.loadNoData(); return; @@ -855,33 +857,31 @@ class _ChatDetailsPage extends State SizedBox( width: 12.w, ), - Expanded( - // flex: 3, - child: Container( - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(6), - // color: Color(0xFFFFFFFF), - // boxShadow: [ - // BoxShadow( - // color: Color(0xFFA8A3A3).withAlpha(12), - // offset: Offset(0, 4), - // blurRadius: 4, - // spreadRadius: 0, - // ), - // ], - // ), - child: GestureDetector( - onLongPress: () { - setState(() {}); - }, - child: Image.file( - File(messages[position].attach), - width: 180.h, - height: 200.h, - fit: BoxFit.contain, - ), - ), - ), + GestureDetector( + onLongPress: () { + setState(() {}); + }, + onTap:(){}, + child: FutureBuilder( + future: fetchImageSize(messages[position].attach), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { + return Image.file( + File(messages[position].attach), + width: 180.w, + height: 200.h, + fit: BoxFit.contain, + ); + } else { + return Image.file( + File(messages[position].attach), + width: snapshot.data.width, + height: snapshot.data.height, + fit: BoxFit.cover, + ); + } + }, + ), ), Spacer(), ], @@ -895,34 +895,31 @@ class _ChatDetailsPage extends State child: Row( children: [ Spacer(), - Expanded( - // flex: 3, - child: Container( - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(6), - // color: Color(0xFFFFFFFF), - // boxShadow: [ - // BoxShadow( - // color: Color(0xFFA8A3A3).withAlpha(12), - // offset: Offset(0, 4), - // blurRadius: 4, - // spreadRadius: 0, - // ), - // ], - // ), - child: GestureDetector( - onLongPress: () { - setState(() {}); - }, - onTap:(){}, - child: Image.file( - File(messages[position].attach), - width: 180.h, - height: 200.h, - fit: BoxFit.contain, - ), - ), - ), + GestureDetector( + onLongPress: () { + setState(() {}); + }, + onTap:(){}, + child: FutureBuilder( + future: fetchImageSize(messages[position].attach), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { + return Image.file( + File(messages[position].attach), + width: 180.w, + height: 200.h, + fit: BoxFit.contain, + ); + } else { + return Image.file( + File(messages[position].attach), + width: snapshot.data.width, + height: snapshot.data.height, + fit: BoxFit.cover, + ); + } + }, + ), ), SizedBox( width: 12.w, @@ -959,6 +956,34 @@ class _ChatDetailsPage extends State ); } + Future fetchImageSize(String imagePath) async { + Size size = Size.zero; + Completer completer = Completer(); + Image.file( + File(imagePath), + ).image.resolve(ImageConfiguration()) + .addListener(ImageStreamListener((image, synchronousCall) { + size = Size((image.image.width ?? 0).toDouble(), (image.image.height ?? 0).toDouble()); + size = resizeImage(size); + completer.complete(size); + })); + return completer.future; + } + + // + resizeImage(Size size) { + if (size.width > 180 || size.height > 200) { + if (size.width > size.height) { + int height = size.height ~/ (size.width ~/ 180); + size = Size(180, height.toDouble()); + } else { + int width = size.width ~/ (size.height ~/ 200); + size = Size(width.toDouble(), 200); + } + } + return size; + } + ///富文本输入框 Widget input() { return Container( From 56ad10156cda208c49ce8ae191de3004253d121c Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 16:13:56 +0800 Subject: [PATCH 67/81] image or other file message send use qiniu, and only send fileUrl. --- lib/constant.dart | 15 ++++++ lib/im/SocketClient.dart | 56 +++++++--------------- lib/im/chat_details_page.dart | 24 ++++------ lib/main.dart | 4 +- lib/retrofit/data/address.dart | 2 +- lib/retrofit/min_api.dart | 4 +- lib/retrofit/retrofit_api.dart | 11 ++--- lib/retrofit/retrofit_api.g.dart | 23 +++++++++ lib/setting/about_page.dart | 3 +- lib/utils/qiniu.dart | 80 ++++++++++++++++++++++++++++++++ pubspec.lock | 50 ++++++++------------ pubspec.yaml | 8 ++-- 12 files changed, 179 insertions(+), 101 deletions(-) create mode 100644 lib/utils/qiniu.dart diff --git a/lib/constant.dart b/lib/constant.dart index ca0bfd1a..5acc9557 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -1,4 +1,19 @@ +import 'package:flutter/foundation.dart'; + +const String chatImageHost = "http://skk8mlm5b.hn-bkt.clouddn.com/"; + +//47.93.216.24:9090 线上 192.168.10.129:9090 测试 +const String socketHost = kDebugMode ? '192.168.10.129' : '47.93.216.24'; +const num socketPort = 9090; + +const ipBaseUrl = "http://whois.pconline.com.cn"; + +const localMiniBaseUrl = "http://192.168.10.54:8765/app/";///本地 +const serviceMiniBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 + +const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 +const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 Map> groupBy(Iterable values, S Function(T) key) { diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index 01f7f05f..f35df628 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; -import 'dart:core'; import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/im/Proto.dart'; import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/out/auth.pb.dart'; @@ -17,9 +18,6 @@ import 'package:shared_preferences/shared_preferences.dart'; class SocketClient { - //47.93.216.24:9090 线上 192.168.10.129:9090 测试 - final String ip = kDebugMode ? '192.168.10.129' : '47.93.216.24'; - final num port = 9090; Socket _socket; SharedPreferences _shared; Timer timer; @@ -34,8 +32,8 @@ class SocketClient { } showDebugToast("socket-connect .... "); - await Socket.connect(ip, port).then((value) { - debugPrint("socket-connect-$ip"); + await Socket.connect(socketHost, socketPort).then((value) { + debugPrint("socket-connect-$socketHost"); _socket = value; _socket.listen((data) { print(data); @@ -77,34 +75,18 @@ class SocketClient { } Future> receiveInsertMessage(MsgData dataResult) async { - String content = ""; - String attach = ""; Uint8List dataBytes = dataResult.data; + String content = utf8.decode(dataBytes); + + String attach = ""; MsgType type = MsgType.values[dataResult.type.value]; - if (type == MsgType.TEXT) { - content = utf8.decode(dataBytes); - } else if (type == MsgType.IMAGE || type == MsgType.VIDEO) { - Map result = await ImageGallerySaver.saveImage( - dataBytes, - isReturnImagePathOfIOS: true, - ); + if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { + + String filePath = await qiniu.downloadFile(content); + + Map result = await ImageGallerySaver.saveFile(filePath).catchError((error){}); bool isSuccess = result["isSuccess"] != null && result["isSuccess"]; - if (isSuccess) { - attach = result["filePath"]; - } - } else if (type == MsgType.AUDIO) { - Directory dir = await getTemporaryDirectory(); - File file = File("${dir.path}/hx_${DateTime.now().millisecondsSinceEpoch}.wav"); - try { - IOSink ioSink = file.openWrite(); - ioSink.write(dataBytes); - ioSink.close(); - attach = file.path; - } catch (e) { - debugPrint("${file.path} write fail"); - } - } else { - content = "暂不支持的消息格式"; + attach = filePath; } Map messageMap = createMessage(userId, content, attach: attach, msgType: type.value, fromId: dataResult.from); @@ -210,19 +192,13 @@ class SocketClient { Future sendMessage(String toId, String content, {String attach, int msgType = 1, replyId}) async { MsgType type = MsgType.values[msgType]; - Uint8List data; - if (type == MsgType.TEXT) { - data = utf8.encode(content); - } else if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { + Uint8List data = utf8.encode(content); + if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) { File file = File(attach); Directory dir = await getTemporaryDirectory(); - File newFile = await file.copy("${dir.path}/hx_${DateTime.now().millisecondsSinceEpoch}.${file.path.split(".")[1]}"); - data = await file.readAsBytes(); + File newFile = await file.copy("${dir.path}/hx_${attach.split('/').last}"); attach = newFile.path; - } else { - data = utf8.encode(content); } - Map message = createMessage(toId, content, fromId: userId, attach: attach, msgType: msgType, replyId: replyId); message["state"] = 1; int id = await hxDatabase.insert(message).catchError((error) { diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 0e01fae5..e99971cc 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -5,9 +5,7 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -15,17 +13,16 @@ import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; +import 'package:huixiang/utils/qiniu.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:huixiang/view_widget/my_footer.dart'; import 'package:image_pickers/image_pickers.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; + import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; -import '../community/photo_view_gallery_screen.dart'; import '../retrofit/data/base_data.dart'; import '../retrofit/data/user_info.dart'; import '../utils/flutter_utils.dart'; @@ -137,6 +134,8 @@ class _ChatDetailsPage extends State ///查询个人信息 queryUser() async { final SharedPreferences value = await SharedPreferences.getInstance(); + if (apiService == null) + apiService = ApiService(Dio(), context: context, token: value.getString("token")); if (value.containsKey('user') && value.getString('user') != null && value.getString('user') != "") { @@ -146,9 +145,6 @@ class _ChatDetailsPage extends State return; } } - if (apiService == null) - apiService = - ApiService(Dio(), context: context, token: value.getString("token")); BaseData baseData = await apiService.queryInfo().catchError((onError) {}); if (baseData != null && baseData.isSuccess) { @@ -317,8 +313,9 @@ class _ChatDetailsPage extends State } List filePath = mediaPaths.map((e) => e.path).toList(); - filePath.forEach((path) { - socketClient.sendMessage(_toUser.mid, "", attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4); + Future.forEach(filePath.toSet(), (path) async { + String fileUrl = await qiniu.uploadFile(apiService, path); + socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4); }); } @@ -970,7 +967,6 @@ class _ChatDetailsPage extends State return completer.future; } - // resizeImage(Size size) { if (size.width > 180 || size.height > 200) { if (size.width > size.height) { @@ -1063,9 +1059,9 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - // _onMoreTap(); - // jumpToBottom(); - SmartDialog.showToast("暂不支持", alignment: Alignment.center); + _onMoreTap(); + jumpToBottom(); + // SmartDialog.showToast("暂不支持", alignment: Alignment.center); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), diff --git a/lib/main.dart b/lib/main.dart index bb72c565..99492ef7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -78,6 +78,7 @@ import 'package:huixiang/test_page.dart'; import 'package:huixiang/union/location_map_page.dart'; import 'package:huixiang/union/union_select_city.dart'; import 'package:huixiang/utils/ImgCachePath.dart'; +import 'package:huixiang/utils/qiniu.dart'; import 'package:huixiang/vip/user_vip_service_page.dart'; import 'package:huixiang/web/web_page.dart'; @@ -235,7 +236,8 @@ closeDatabase() async { } final SocketClient socketClient = new SocketClient(); -EventBus eventBus = EventBus(sync: true); +final Qiniu qiniu = Qiniu(); +final EventBus eventBus = EventBus(sync: true); Route lastRoutePage; class MyApp extends StatelessWidget { diff --git a/lib/retrofit/data/address.dart b/lib/retrofit/data/address.dart index e571e2cb..8065d54d 100644 --- a/lib/retrofit/data/address.dart +++ b/lib/retrofit/data/address.dart @@ -20,7 +20,7 @@ class Address { factory Address.fromJson(Map json) => Address() ..address = json['address'] as String ..area = json['area'] as String - ..city = json['city'] as String + ..city = (json['city'] is List) ? [] : json['city'] as String ..cityInfo = json['cityInfo'] as String ..id = json['id'] as String ..isDefault = json['isDefault'] as bool diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index d454a7eb..011cceca 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/retrofit/data/address.dart'; import 'package:huixiang/retrofit/data/base_data.dart'; @@ -27,9 +28,6 @@ import 'data/shopping_home_config.dart'; part 'min_api.g.dart'; -const localBaseUrl = "http://192.168.10.54:8765/app/";///本地 -// const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///测试 -const serviceBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 ///调用小程序的接口 diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index a7a07276..e51600fb 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -7,6 +7,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/retrofit/data/activity.dart'; import 'package:huixiang/retrofit/data/article.dart'; @@ -66,17 +67,10 @@ import 'data/vip_benefit_list.dart'; import 'data/vip_card.dart'; import 'data/vip_card_home.dart'; import 'data/vip_rule_details.dart'; -import 'data/wx_pay.dart'; part 'retrofit_api.g.dart'; -const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 -// const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///测试 -const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 -const ipBaseUrl = "http://whois.pconline.com.cn"; - -///ip @RestApi(baseUrl: localBaseUrl) abstract class ApiService { @@ -664,4 +658,7 @@ abstract class ApiService { Future>> memberSearch( @Path("keyword") String keyword); + @GET("/config/qiniuToken") + Future> getQiniuToken(); + } diff --git a/lib/retrofit/retrofit_api.g.dart b/lib/retrofit/retrofit_api.g.dart index 52182e80..47009986 100644 --- a/lib/retrofit/retrofit_api.g.dart +++ b/lib/retrofit/retrofit_api.g.dart @@ -2576,4 +2576,27 @@ class _ApiService implements ApiService { .toList()); return value; } + + @override + Future> getQiniuToken() async { + const _extra = {}; + final queryParameters = {}; + final _data = {}; + final _result = await _dio.request>( + '/config/qiniuToken', + queryParameters: queryParameters, + options: RequestOptions( + method: 'GET', + headers: {}, + extra: _extra, + baseUrl: baseUrl, + ), + data: _data, + ); + final value = BaseData.fromJson( + _result.data, + (json) => json); + return value; + } + } diff --git a/lib/setting/about_page.dart b/lib/setting/about_page.dart index 36e38398..fc381a3e 100644 --- a/lib/setting/about_page.dart +++ b/lib/setting/about_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:huixiang/constant.dart'; import 'package:huixiang/generated/l10n.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/main.dart'; @@ -87,7 +88,7 @@ class _AboutPage extends State { height: 20.h, ), if (kDebugMode) - textItem("ip: ${socketClient.ip}\nport: ${socketClient.port}\nid: ${socketClient.userId}"), + textItem("ip: $socketHost\nport: $socketPort\nid: ${socketClient.userId}"), GestureDetector( onTap: (){ // updateApp(); diff --git a/lib/utils/qiniu.dart b/lib/utils/qiniu.dart new file mode 100644 index 00000000..bfcdb6a8 --- /dev/null +++ b/lib/utils/qiniu.dart @@ -0,0 +1,80 @@ + + + +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:huixiang/constant.dart'; +import 'package:huixiang/retrofit/data/base_data.dart'; +import 'package:huixiang/retrofit/retrofit_api.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart'; + +class Qiniu { + + + Storage storage = Storage(config: Config()); + PutController putController = PutController(); + + progressListener() { + // 添加任务进度监听 + putController.addProgressListener((double percent) { + print('任务进度变化:已发送:$percent'); + }); + + // 添加文件发送进度监听 + putController.addSendProgressListener((double percent) { + print('已上传进度变化:已发送:$percent'); + }); + + // 添加任务状态监听 + putController.addStatusListener((StorageStatus status) { + print('状态变化: 当前任务状态:$status'); + }); + + } + + Future uploadFile(ApiService apiService, String filePath) async { + + String token = await _getToken(apiService); + + File file = File(filePath); + + PutResponse putResponse = await storage.putFile(file, token, options: PutOptions( + controller: putController, + key: filePath.split('/').last, + )); + + debugPrint("qiniuToken-result: ${putResponse.toJson()}"); + + return "$chatImageHost/${putResponse.key}"; + } + + Future _getToken(ApiService apiService) async { + BaseData baseData = await apiService.getQiniuToken() + .catchError((error){ + debugPrint("getQiniuToken: $error"); + }); + if (baseData.isSuccess) { + return baseData.data; + } else { + debugPrint("get token fail, check network"); + throw Error(); + } + } + + Dio dio = Dio(); + + Future downloadFile(String urlPath) async { + Directory dir = await getTemporaryDirectory(); + File newFile = File("${dir.path}/hx_${urlPath.split('/').last}"); + Response response = await dio.download(urlPath, newFile.path); + if (response.statusCode == 200) { + return newFile.path; + } + return null; + } + +} + diff --git a/pubspec.lock b/pubspec.lock index e107bfab..d33b4155 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,14 +25,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" async: dependency: transitive description: @@ -141,18 +133,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: "3fa83bc417e26f1d55814443a603747d48e3cdf55b2f4fea27dea8e9224dcefd" url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.1" + version: "2.1.1" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "3ce628f3c6a7144be6c524dbb20ca5af52fa59c850c587bf5e52cd70ece8e7e8" url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.3" + version: "2.1.5" csslib: dependency: transitive description: @@ -185,14 +177,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" - encrypt: - dependency: "direct main" - description: - name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.0.1" equatable: dependency: transitive description: @@ -573,14 +557,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.0" - mqtt_client: - dependency: "direct main" - description: - name: mqtt_client - sha256: ba10ec490ded55dc4e77bbc992529d823fb15d0d5ec68c2895f960312060c541 - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.8.1" nested: dependency: transitive description: @@ -761,10 +737,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: a74ed4039c152dd319514f142768d93ad3973860b240d90160cec727c8155f65 url: "https://pub.flutter-io.cn" source: hosted - version: "3.7.3" + version: "3.0.1" protobuf: dependency: "direct main" description: @@ -789,6 +765,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + qiniu_flutter_sdk: + dependency: "direct main" + description: + path: "qiniu-dart-sdk/flutter" + relative: true + source: path + version: "0.2.0" + qiniu_sdk_base: + dependency: transitive + description: + path: "qiniu-dart-sdk/base" + relative: true + source: path + version: "0.2.2" qr: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e3145f3a..9431a255 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: visibility_detector: ^0.3.3 steel_crypt: ^3.0.0+1 - encrypt: ^5.0.1 +# encrypt: any event_bus: ^2.0.0 intl: ^0.17.0 @@ -115,18 +115,18 @@ dependencies: emoji_picker_flutter: ^1.4.1 - mqtt_client: ^9.6.8 - shimmer: ^3.0.0 # 时间选择器 flutter_datetime_picker: ^1.5.1 + qiniu_flutter_sdk: + path: qiniu-dart-sdk/flutter + widgetpicker: ^0.1.1 # floor: ^1.4.2 sqflite: ^2.2.2 syncfusion_flutter_datepicker: ^19.4.38 - protobuf: ^3.1.0 dev_dependencies: From d3b44ac15cc43554eddf29862d1c2d1f94743d65 Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 16:33:59 +0800 Subject: [PATCH 68/81] add qiniu plugins --- qiniu-dart-sdk/.travis.yml | 58 +++ qiniu-dart-sdk/CODE_OF_CONDUCT.md | 76 +++ qiniu-dart-sdk/README.md | 15 + qiniu-dart-sdk/base/CHANGELOG.md | 12 + qiniu-dart-sdk/base/LICENSE | 201 ++++++++ qiniu-dart-sdk/base/README.md | 29 ++ qiniu-dart-sdk/base/analysis_options.yaml | 90 ++++ qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart | 5 + qiniu-dart-sdk/base/lib/src/auth/auth.dart | 125 +++++ .../base/lib/src/auth/put_policy.dart | 209 ++++++++ qiniu-dart-sdk/base/lib/src/error/error.dart | 12 + .../base/lib/src/storage/config/cache.dart | 39 ++ .../base/lib/src/storage/config/config.dart | 28 ++ .../base/lib/src/storage/config/host.dart | 124 +++++ .../base/lib/src/storage/config/protocol.dart | 11 + .../base/lib/src/storage/error/error.dart | 75 +++ .../methods/put/by_part/cache_mixin.dart | 20 + .../put/by_part/complete_parts_task.dart | 51 ++ .../methods/put/by_part/init_parts_task.dart | 92 ++++ .../src/storage/methods/put/by_part/part.dart | 26 + .../put/by_part/put_by_part_options.dart | 25 + .../methods/put/by_part/put_parts_task.dart | 187 +++++++ .../methods/put/by_part/upload_part_task.dart | 112 +++++ .../put/by_part/upload_parts_task.dart | 260 ++++++++++ .../put/by_single/put_by_single_options.dart | 13 + .../put/by_single/put_by_single_task.dart | 60 +++ .../base/lib/src/storage/methods/put/put.dart | 5 + .../storage/methods/put/put_controller.dart | 3 + .../src/storage/methods/put/put_options.dart | 28 ++ .../src/storage/methods/put/put_response.dart | 33 ++ .../base/lib/src/storage/status/status.dart | 21 + .../base/lib/src/storage/storage.dart | 99 ++++ .../lib/src/storage/task/request_task.dart | 207 ++++++++ .../storage/task/request_task_controller.dart | 97 ++++ .../storage/task/request_task_manager.dart | 15 + .../base/lib/src/storage/task/task.dart | 50 ++ .../lib/src/storage/task/task_manager.dart | 66 +++ qiniu-dart-sdk/base/pubspec.lock | 461 ++++++++++++++++++ qiniu-dart-sdk/base/pubspec.yaml | 18 + qiniu-dart-sdk/codecov.yml | 11 + qiniu-dart-sdk/flutter/CHANGELOG.md | 8 + qiniu-dart-sdk/flutter/LICENSE | 201 ++++++++ qiniu-dart-sdk/flutter/README.md | 182 +++++++ qiniu-dart-sdk/flutter/analysis_options.yaml | 90 ++++ .../flutter/lib/qiniu_flutter_sdk.dart | 3 + .../flutter/lib/src/storage/controller.dart | 3 + .../flutter/lib/src/storage/storage.dart | 33 ++ qiniu-dart-sdk/flutter/pubspec.lock | 228 +++++++++ qiniu-dart-sdk/flutter/pubspec.yaml | 23 + 49 files changed, 3840 insertions(+) create mode 100644 qiniu-dart-sdk/.travis.yml create mode 100644 qiniu-dart-sdk/CODE_OF_CONDUCT.md create mode 100644 qiniu-dart-sdk/README.md create mode 100644 qiniu-dart-sdk/base/CHANGELOG.md create mode 100644 qiniu-dart-sdk/base/LICENSE create mode 100644 qiniu-dart-sdk/base/README.md create mode 100644 qiniu-dart-sdk/base/analysis_options.yaml create mode 100644 qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart create mode 100644 qiniu-dart-sdk/base/lib/src/auth/auth.dart create mode 100644 qiniu-dart-sdk/base/lib/src/auth/put_policy.dart create mode 100644 qiniu-dart-sdk/base/lib/src/error/error.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/config/cache.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/config/config.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/config/host.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/error/error.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/methods/put/put_response.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/status/status.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/storage.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/task/request_task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/task/request_task_controller.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/task/request_task_manager.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/task/task.dart create mode 100644 qiniu-dart-sdk/base/lib/src/storage/task/task_manager.dart create mode 100644 qiniu-dart-sdk/base/pubspec.lock create mode 100644 qiniu-dart-sdk/base/pubspec.yaml create mode 100644 qiniu-dart-sdk/codecov.yml create mode 100644 qiniu-dart-sdk/flutter/CHANGELOG.md create mode 100644 qiniu-dart-sdk/flutter/LICENSE create mode 100644 qiniu-dart-sdk/flutter/README.md create mode 100644 qiniu-dart-sdk/flutter/analysis_options.yaml create mode 100644 qiniu-dart-sdk/flutter/lib/qiniu_flutter_sdk.dart create mode 100644 qiniu-dart-sdk/flutter/lib/src/storage/controller.dart create mode 100644 qiniu-dart-sdk/flutter/lib/src/storage/storage.dart create mode 100644 qiniu-dart-sdk/flutter/pubspec.lock create mode 100644 qiniu-dart-sdk/flutter/pubspec.yaml diff --git a/qiniu-dart-sdk/.travis.yml b/qiniu-dart-sdk/.travis.yml new file mode 100644 index 00000000..f3275a6f --- /dev/null +++ b/qiniu-dart-sdk/.travis.yml @@ -0,0 +1,58 @@ +language: dart + +dart: + - stable + +install: + - cd $TRAVIS_BUILD_DIR/base && pub get + # - cd $$TRAVIS_BUILD_DIR/flutter && pub get + +before_script: + - cd $TRAVIS_BUILD_DIR/base && rm -rf .env + +jobs: + include: + ####################################### + ######### jobs for base ############# + ####################################### + # 检查 lint 并在 warnings、infos 时报错退出 + - stage: base(analyze,format,test) + name: "Analyze" + os: linux + script: cd $TRAVIS_BUILD_DIR/base && dartanalyzer --fatal-warnings --fatal-infos . + # 检查格式并在异常时退出 + - stage: base(analyze,format,test) + name: "Format" + os: linux + script: cd $TRAVIS_BUILD_DIR/base && dartfmt -n --set-exit-if-changed . + # 执行测试(已开启 null safe) + - stage: base(analyze,format,test) + name: "Vm Tests" + os: linux + script: cd $TRAVIS_BUILD_DIR/base && pub run test_coverage --print-test-output && bash <(curl -s https://codecov.io/bash) + + ####################################### + ####### jobs for flutter_sdk ########## + ####################################### + # - stage: flutter_sdk(analyze,format,test) + # name: "Analyze" + # os: linux + # script: cd $TRAVIS_BUILD_DIR/flutter + # - stage: flutter_sdk(analyze,format,test) + # name: "Format" + # os: linux + # script: cd $TRAVIS_BUILD_DIR/flutter + + # - stage: flutter_sdk(analyze,format,test) + # name: "Vm Tests" + # os: linux + # script: cd $TRAVIS_BUILD_DIR/flutter + +stages: + - base(analyze,format,test) + # - flutter_sdk(analyze,format,test) + # - flutter_sdk_example(analyze,format,test) + +cache: + directories: + - $HOME/.pub-cache diff --git a/qiniu-dart-sdk/CODE_OF_CONDUCT.md b/qiniu-dart-sdk/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..aedf0c68 --- /dev/null +++ b/qiniu-dart-sdk/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at yinxulai@qiniu.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/qiniu-dart-sdk/README.md b/qiniu-dart-sdk/README.md new file mode 100644 index 00000000..21833b49 --- /dev/null +++ b/qiniu-dart-sdk/README.md @@ -0,0 +1,15 @@ +# Dart SDK + +[![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base) +[![qiniu_flutter_sdk](https://img.shields.io/pub/v/qiniu_flutter_sdk.svg?label=qiniu_flutter_sdk)](https://pub.dev/packages/qiniu_flutter_sdk) + +## 目录说明 + +- base 封装了七牛各业务的基础实现 +- flutter 该目录是 base + Flutter 的绑定实现,同时导出为单独的 package 提供给用户使用 + +### [Flutter SDK](https://github.com/qiniu/dart-sdk/tree/master/flutter) + +七牛云业务基于 Dart 绑定 Flutter 的实现,为 Flutter 提供简易的使用方式,更多信息查看该目录下的 [README.md](https://github.com/qiniu/dart-sdk/tree/master/flutter/README.md) 文件。 diff --git a/qiniu-dart-sdk/base/CHANGELOG.md b/qiniu-dart-sdk/base/CHANGELOG.md new file mode 100644 index 00000000..3ee9fdda --- /dev/null +++ b/qiniu-dart-sdk/base/CHANGELOG.md @@ -0,0 +1,12 @@ +## 0.1.0 + +- Initial Release. + +## 0.2.0 + +- 优化了 `StorageError` 输出的调用栈 +- `CacheProvider` 的方法都改成异步的 + +## 0.2.1 + +- 修复关闭 App 缓存丢失的问题 diff --git a/qiniu-dart-sdk/base/LICENSE b/qiniu-dart-sdk/base/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/qiniu-dart-sdk/base/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/qiniu-dart-sdk/base/README.md b/qiniu-dart-sdk/base/README.md new file mode 100644 index 00000000..1b4bd83a --- /dev/null +++ b/qiniu-dart-sdk/base/README.md @@ -0,0 +1,29 @@ +# Qiniu Sdk Base [![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base) [![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk) + +七牛 dart 平台 sdk 的 base 包,为上层 sdk 提供基础设施和共享代码。 + +## 功能列表 + +* 单文件上传 +* 分片上传 +* 任务状态 +* 任务进度 +* 上传进度 +* 失败重试 + +## 如何测试 + +创建 `.env` 文件,并输入如下内容 + +``` +export QINIU_DART_SDK_ACCESS_KEY= +export QINIU_DART_SDK_SECRET_KEY= +export QINIU_DART_SDK_TOKEN_SCOPE= +``` + + +在 `.env` 文件中填好敏感数据,即 ak、sk、scope + +接着运行如下指令 + +`pub run test` diff --git a/qiniu-dart-sdk/base/analysis_options.yaml b/qiniu-dart-sdk/base/analysis_options.yaml new file mode 100644 index 00000000..96c1e8bf --- /dev/null +++ b/qiniu-dart-sdk/base/analysis_options.yaml @@ -0,0 +1,90 @@ +# copy from https://github.com/dart-lang/http/blob/master/analysis_options.yaml + +include: package:pedantic/analysis_options.yaml + +analyzer: + # enable-experiment: + # - non-nullable + + strong-mode: + implicit-casts: false + implicit-dynamic: false + +linter: + rules: + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cascade_invocations + # comment_references + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - hash_and_equals + - invariant_booleans + - iterable_contains_unrelated_type + - library_names + - library_prefixes + - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_conditional_assignment + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_collection_literals + - prefer_generic_function_type_aliases + - prefer_initializing_formals + - prefer_is_empty + - prefer_is_not_empty + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - valid_regexps + - void_checks diff --git a/qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart b/qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart new file mode 100644 index 00000000..b0abc803 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart @@ -0,0 +1,5 @@ +library qiniu_sdk_base; + +export 'src/auth/auth.dart'; +export 'src/error/error.dart'; +export 'src/storage/storage.dart'; diff --git a/qiniu-dart-sdk/base/lib/src/auth/auth.dart b/qiniu-dart-sdk/base/lib/src/auth/auth.dart new file mode 100644 index 00000000..90b7feff --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/auth/auth.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:crypto/crypto.dart'; +import 'package:meta/meta.dart'; +import './put_policy.dart'; + +export 'put_policy.dart'; + +class TokenInfo { + final String accessKey; + final PutPolicy putPolicy; + const TokenInfo(this.accessKey, this.putPolicy); +} + +/// 提供用于鉴权的相关功能。 +/// +/// 更多信息请查看[官方文档-安全机制](https://developer.qiniu.com/kodo/manual/1644/security) +class Auth { + /// 鉴权所需的 [accessKey]。 + /// + /// 更多信息请查看[官方文档-密钥 AccessKey/SecretKey](https://developer.qiniu.com/kodo/manual/1644/security#aksk) + /// 使用须知请查看[官方文档-密钥安全使用须知](https://developer.qiniu.com/kodo/kb/1334/the-access-key-secret-key-encryption-key-safe-use-instructions) + final String accessKey; + + /// 鉴权所需的 [secretKey]。 + /// + /// 如何生成以及查看、使用等请参阅 [accessKey] 的说明 + final String secretKey; + + const Auth({ + @required this.accessKey, + @required this.secretKey, + }) : assert(accessKey != null), + assert(secretKey != null); + + /// 根据上传策略生成上传使用的 Token。 + /// + /// 具体的上传策略说明请参考 [PutPolicy] 模块 + String generateUploadToken({ + @required PutPolicy putPolicy, + }) { + assert(putPolicy != null); + + var data = jsonEncode(putPolicy); + var encodedPutPolicy = base64Url.encode(utf8.encode(data)); + var baseToken = generateAccessToken(bytes: utf8.encode(encodedPutPolicy)); + return '$baseToken:$encodedPutPolicy'; + } + + /// 生成针对私有空间资源的下载 Token。 + /// + /// [key] 为对象的名称 + /// [deadline] 有效时间,单位为秒,例如 1451491200 + /// [bucketDomain] 空间绑定的域名,例如 http://test.bucket.com + String generateDownloadToken({ + @required String key, + @required int deadline, + @required String bucketDomain, + }) { + assert(key != null); + assert(deadline != null); + assert(bucketDomain != null); + + var downloadURL = '$bucketDomain/$key?e=$deadline'; + return generateAccessToken(bytes: utf8.encode(downloadURL)); + } + + /// 根据数据签名,生成 Token(用于接口的访问鉴权)。 + /// + /// 访问七牛的接口需要对请求进行签名, 该方法提供 Token 签发服务 + String generateAccessToken({@required List bytes}) { + assert(bytes != null); + + var hmacEncoder = Hmac(sha1, utf8.encode(secretKey)); + + var sign = hmacEncoder.convert(bytes); + var encodedSign = base64Url.encode(sign.bytes); + return '$accessKey:$encodedSign'; + } + + /// 解析 token 信息。 + /// + /// 从 Token 字符串中解析 [accessKey]、[PutPolicy] 信息 + static TokenInfo parseToken(String token) { + assert(token != null && token != ''); + + var segments = token.split(':'); + if (segments.length < 2) { + throw ArgumentError('invalid token'); + } + + PutPolicy putPolicy; + var accessKey = segments.first; + + /// 具体的 token 信息可以参考这里。 + /// [内部文档](https://github.com/qbox/product/blob/master/kodo/auths/UpToken.md#admin-uptoken-authorization) + if (segments.length >= 3) { + if (segments.last == '') { + throw ArgumentError('invalid token'); + } + + putPolicy = PutPolicy.fromJson(jsonDecode( + String.fromCharCodes( + base64Url.decode( + segments.last, + ), + ), + ) as Map); + } + + return TokenInfo(accessKey, putPolicy); + } + + /// 解析 up token 信息。 + /// + /// 从 Token 字符串中解析 [accessKey]、[PutPolicy] 信息 + static TokenInfo parseUpToken(String token) { + assert(token != null && token != ''); + final tokenInfo = parseToken(token); + if (tokenInfo.putPolicy == null) { + throw ArgumentError('invalid up token'); + } + + return tokenInfo; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/auth/put_policy.dart b/qiniu-dart-sdk/base/lib/src/auth/put_policy.dart new file mode 100644 index 00000000..77b20cbb --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/auth/put_policy.dart @@ -0,0 +1,209 @@ +import 'package:meta/meta.dart'; + +/// 上传策略 +/// +/// 更多信息请查看[官方文档-上传策略](https://developer.qiniu.com/kodo/manual/1206/put-policy) +class PutPolicy { + /// 指定上传的目标资源空间 Bucket 和资源键 Key(最大为 750 字节)。 + /// + /// 有三种格式: + /// 表示允许用户上传文件到指定的 Bucket,在这种格式下文件只能新增。 + /// : 表示只允许用户上传指定 Key 的文件。在这种格式下文件默认允许修改。 + /// : 表示只允许用户上传指定以 KeyPrefix 为前缀的文件。 + /// 具体信息一定请查看上述的上传策略文档! + final String scope; + + /// 获取 Bucket。 + /// + /// 从 [scope] 中获取 Bucket。 + String getBucket() { + return scope.split(':').first; + } + + /// 若为 1,表示允许用户上传以 [scope] 的 KeyPrefix 为前缀的文件。 + final int isPrefixalScope; + + /// 上传凭证有效截止时间。 + /// + /// Unix 时间戳,单位为秒, + /// 该截止时间为上传完成后,在七牛空间生成文件的校验时间, 而非上传的开始时间, + /// 一般建议设置为上传开始时间 + 3600s。 + final int deadline; + + /// 限制为新增文件。 + /// + /// 如果设置为非 0 值,则无论 [scope] 设置为什么形式,仅能以新增模式上传文件。 + final int insertOnly; + + /// 唯一属主标识。 + /// + /// 特殊场景下非常有用,例如根据 App-Client 标识给图片或视频打水印。 + final String endUser; + + /// Web 端文件上传成功后,浏览器执行 303 跳转的 URL。 + /// + /// 文件上传成功后会跳转到 <[returnUrl]>?upload_ret= + /// 其中 包含 [returnBody] 内容。 + /// 如不设置 [returnUrl],则直接将 [returnBody] 的内容返回给客户端。 + final String returnUrl; + + /// [returnBody] 声明服务端的响应格式。 + /// + /// 可以使用 <魔法变量> 和 <自定义变量>,必须是合法的 JSON 格式, + /// 关于 <魔法变量> 请参阅:[官方文档-魔法变量](https://developer.qiniu.com/kodo/manual/1235/vars#magicvar) + /// 关于 <自定义变量> 请参阅:[官方文档-自定义变量](https://developer.qiniu.com/kodo/manual/1235/vars#xvar) + final String returnBody; + + /// 上传成功后,七牛云向业务服务器发送 POST 请求的 URL。 + final String callbackUrl; + + /// 上传成功后,七牛云向业务服务器发送回调通知时的 Host 值。 + /// + /// 与 [callbackUrl] 配合使用,仅当设置了 [callbackUrl] 时才有效。 + final String callbackHost; + + /// 上传成功后发起的回调请求。 + /// + /// 七牛云向业务服务器发送 Content-Type: application/x-www-form-urlencoded 的 POST 请求, + /// 例如:{"key":"$(key)","hash":"$(etag)","w":"$(imageInfo.width)","h":"$(imageInfo.height)"}, + /// 可以使用 <魔法变量> 和 <自定义变量>。 + final String callbackBody; + + /// 上传成功后发起的回调请求的 Content-Type。 + /// + /// 默认为 application/x-www-form-urlencoded,也可设置为 application/json。 + final String callbackBodyType; + + /// 资源上传成功后触发执行的预转持久化处理指令列表。 + /// + /// [fileType] = 2(上传归档存储文件)时,不支持使用该参数, + /// 每个指令是一个 API 规格字符串,多个指令用 ; 分隔, + /// 可以使用 <魔法变量> 和 <自定义变量>, + /// 改字段的具体使用信息可以查看:[官方文档-#persistentOps](https://developer.qiniu.com/kodo/manual/1206/put-policy#persistentOps) + final String persistentOps; + + /// 接收持久化处理结果通知的 URL。 + /// + /// 必须是公网上可以正常进行 POST 请求并能响应 HTTP/1.1 200 OK 的有效 URL, + /// 该 URL 获取的内容和持久化处理状态查询的处理结果一致, + /// 发送 body 格式是 Content-Type 为 application/json 的 POST 请求, + /// 需要按照读取流的形式读取请求的 body 才能获取。 + final String persistentNotifyUrl; + + /// 转码队列名。 + /// + /// 资源上传成功后,触发转码时指定独立的队列进行转码, + /// 为空则表示使用公用队列,处理速度比较慢。建议使用专用队列。 + final String persistentPipeline; + + /// [saveKey] 的优先级设置。 + /// + /// 该设置为 true 时,[saveKey] 不能为空,会忽略客户端指定的 Key,强制使用 [saveKey] 进行文件命名。 + /// 参数不设置时,默认值为 false。 + final String forceSaveKey; + + /// 自定义资源名。 + /// + /// 支持<魔法变量>和<自定义变量>, [forceSaveKey] 为 false 时, + /// 这个字段仅当用户上传的时候没有主动指定 key 时起作用, + /// [forceSaveKey] 为 true 时,将强制按这个字段的格式命名。 + final String saveKey; + + /// 限定上传文件大小最小值,单位 Byte。 + final int fsizeMin; + + /// 限定上传文件大小最大值,单位 Byte。 + /// + /// 超过限制上传文件大小的最大值会被判为上传失败,返回 413 状态码。 + final int fsizeLimit; + + /// 开启 MimeType 侦测功能。 + final int detectMime; + + /// 限定用户上传的文件类型。 + final String mimeLimit; + + /// 文件存储类型 + /// + /// 0 为标准存储(默认), + /// 1 为低频存储, + /// 2 为归档存储。 + final int fileType; + + const PutPolicy({ + @required this.scope, + @required this.deadline, + this.isPrefixalScope, + this.insertOnly, + this.endUser, + this.returnUrl, + this.returnBody, + this.callbackUrl, + this.callbackHost, + this.callbackBody, + this.callbackBodyType, + this.persistentOps, + this.persistentNotifyUrl, + this.persistentPipeline, + this.forceSaveKey, + this.saveKey, + this.fsizeMin, + this.fsizeLimit, + this.detectMime, + this.mimeLimit, + this.fileType, + }) : assert(scope != null), + assert(deadline != null); + + Map toJson() { + return { + 'scope': scope, + 'isPrefixalScope': isPrefixalScope, + 'deadline': deadline, + 'insertOnly': insertOnly, + 'endUser': endUser, + 'returnUrl': returnUrl, + 'returnBody': returnBody, + 'callbackUrl': callbackUrl, + 'callbackHost': callbackHost, + 'callbackBody': callbackBody, + 'callbackBodyType': callbackBodyType, + 'persistentOps': persistentOps, + 'persistentNotifyUrl': persistentNotifyUrl, + 'persistentPipeline': persistentPipeline, + 'forceSaveKey': forceSaveKey, + 'saveKey': saveKey, + 'fsizeMin': fsizeMin, + 'fsizeLimit': fsizeLimit, + 'detectMime': detectMime, + 'mimeLimit': mimeLimit, + 'fileType': fileType, + }..removeWhere((key, dynamic value) => value == null); + } + + factory PutPolicy.fromJson(Map json) { + return PutPolicy( + scope: json['scope'] as String, + deadline: json['deadline'] as int, + isPrefixalScope: json['isPrefixalScope'] as int, + insertOnly: json['insertOnly'] as int, + endUser: json['endUser'] as String, + returnUrl: json['returnUrl'] as String, + returnBody: json['returnBody'] as String, + callbackUrl: json['callbackUrl'] as String, + callbackHost: json['callbackHost'] as String, + callbackBody: json['callbackBody'] as String, + callbackBodyType: json['callbackBodyType'] as String, + persistentOps: json['persistentOps'] as String, + persistentNotifyUrl: json['persistentNotifyUrl'] as String, + persistentPipeline: json['persistentPipeline'] as String, + forceSaveKey: json['forceSaveKey'] as String, + saveKey: json['saveKey'] as String, + fsizeMin: json['fsizeMin'] as int, + fsizeLimit: json['fsizeLimit'] as int, + detectMime: json['detectMime'] as int, + mimeLimit: json['mimeLimit'] as String, + fileType: json['fileType'] as int, + ); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/error/error.dart b/qiniu-dart-sdk/base/lib/src/error/error.dart new file mode 100644 index 00000000..f4372ad3 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/error/error.dart @@ -0,0 +1,12 @@ +class QiniuError extends Error { + final Error rawError; + + final String _message; + + String get message => _message ?? rawError?.toString() ?? ''; + + @override + StackTrace get stackTrace => rawError?.stackTrace ?? super.stackTrace; + + QiniuError({this.rawError, String message}) : _message = message; +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/config/cache.dart b/qiniu-dart-sdk/base/lib/src/storage/config/cache.dart new file mode 100644 index 00000000..a5fb2603 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/config/cache.dart @@ -0,0 +1,39 @@ +part of 'config.dart'; + +abstract class CacheProvider { + /// 设置一对数据 + Future setItem(String key, String item); + + /// 根据 key 获取缓存 + Future getItem(String key); + + /// 删除指定 key 的缓存 + Future removeItem(String key); + + /// 清除所有 + Future clear(); +} + +class DefaultCacheProvider extends CacheProvider { + Map value = {}; + + @override + Future clear() async { + value.clear(); + } + + @override + Future getItem(String key) async { + return value[key]; + } + + @override + Future removeItem(String key) async { + value.remove(key); + } + + @override + Future setItem(String key, String item) async { + value[key] = item; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/config/config.dart b/qiniu-dart-sdk/base/lib/src/storage/config/config.dart new file mode 100644 index 00000000..eec3087e --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/config/config.dart @@ -0,0 +1,28 @@ +import 'package:dio/adapter.dart'; +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:qiniu_sdk_base/src/storage/error/error.dart'; + +part 'protocol.dart'; +part 'host.dart'; +part 'cache.dart'; + +class Config { + final HostProvider hostProvider; + final CacheProvider cacheProvider; + final HttpClientAdapter httpClientAdapter; + + /// 重试次数 + /// + /// 各种网络请求失败的重试次数 + final int retryLimit; + + Config({ + HostProvider hostProvider, + CacheProvider cacheProvider, + HttpClientAdapter httpClientAdapter, + this.retryLimit = 3, + }) : hostProvider = hostProvider ?? DefaultHostProvider(), + cacheProvider = cacheProvider ?? DefaultCacheProvider(), + httpClientAdapter = httpClientAdapter ?? DefaultHttpClientAdapter(); +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/config/host.dart b/qiniu-dart-sdk/base/lib/src/storage/config/host.dart new file mode 100644 index 00000000..867f1d37 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/config/host.dart @@ -0,0 +1,124 @@ +part of 'config.dart'; + +abstract class HostProvider { + Future getUpHost({ + @required String accessKey, + @required String bucket, + }); + + bool isFrozen(String host); + + void freezeHost(String host); +} + +class DefaultHostProvider extends HostProvider { + final protocol = Protocol.Https.value; + + final _http = Dio(); + // 缓存的上传区域 + final _stashedUpDomains = <_Domain>[]; + // accessKey:bucket 用此 key 判断是否 up host 需要走缓存 + String _cacheKey; + // 冻结的上传区域 + final List<_Domain> _frozenUpDomains = []; + + @override + Future getUpHost({ + @required String accessKey, + @required String bucket, + }) async { + // 解冻需要被解冻的 host + _frozenUpDomains.removeWhere((domain) => !domain.isFrozen()); + + var _upDomains = <_Domain>[]; + if ('$accessKey:$bucket' == _cacheKey && _stashedUpDomains.isNotEmpty) { + _upDomains.addAll(_stashedUpDomains); + } else { + final url = + '$protocol://api.qiniu.com/v4/query?ak=$accessKey&bucket=$bucket'; + + final res = await _http.get(url); + + final hosts = res.data['hosts'] + .map((dynamic json) => _Host.fromJson(json as Map)) + .cast<_Host>() + .toList() as List<_Host>; + + for (var host in hosts) { + final domainList = host.up['domains'].cast() as List; + final domains = domainList.map((domain) => _Domain(domain)); + _upDomains.addAll(domains); + } + + _cacheKey = '$accessKey:$bucket'; + _stashedUpDomains.addAll(_upDomains); + } + + // 每次都从头遍历一遍,最合适的 host 总是会排在最前面 + for (var index = 0; index < _upDomains.length; index++) { + final availableDomain = _upDomains.elementAt(index); + // 检查看起来可用的 host 是否之前被冻结过 + final frozen = isFrozen(protocol + '://' + availableDomain.value); + + if (!frozen) { + return protocol + '://' + availableDomain.value; + } + } + // 全部被冻结,几乎不存在的情况 + throw StorageError( + type: StorageErrorType.NO_AVAILABLE_HOST, + message: '没有可用的上传域名', + ); + } + + @override + bool isFrozen(String host) { + final uri = Uri.parse(host); + final frozenDomain = _frozenUpDomains.firstWhere( + (domain) => domain.isFrozen() && domain.value == uri.host, + orElse: () => null); + return frozenDomain != null; + } + + @override + void freezeHost(String host) { + // http://example.org + // scheme: http + // host: example.org + final uri = Uri.parse(host); + _frozenUpDomains.add(_Domain(uri.host)..freeze()); + } +} + +class _Host { + String region; + int ttl; + // domains: [] + Map up; + + _Host({this.region, this.ttl, this.up}); + + factory _Host.fromJson(Map json) { + return _Host( + region: json['region'] as String, + ttl: json['ttl'] as int, + up: json['up'] as Map, + ); + } +} + +class _Domain { + int frozenTime = 0; + final _lockTime = 1000 * 60 * 10; + + bool isFrozen() { + return frozenTime + _lockTime > DateTime.now().millisecondsSinceEpoch; + } + + void freeze() { + frozenTime = DateTime.now().millisecondsSinceEpoch; + } + + String value; + _Domain(this.value); +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart b/qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart new file mode 100644 index 00000000..80d3575f --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart @@ -0,0 +1,11 @@ +part of 'config.dart'; + +enum Protocol { Http, Https } + +extension ProtocolExt on Protocol { + String get value { + if (this == Protocol.Http) return 'http'; + if (this == Protocol.Https) return 'https'; + return 'https'; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/error/error.dart b/qiniu-dart-sdk/base/lib/src/storage/error/error.dart new file mode 100644 index 00000000..b4de5f5b --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/error/error.dart @@ -0,0 +1,75 @@ +import 'package:dio/dio.dart'; +import 'package:qiniu_sdk_base/src/error/error.dart'; + +enum StorageErrorType { + /// 连接超时 + CONNECT_TIMEOUT, + + /// 发送超时 + SEND_TIMEOUT, + + /// 接收超时 + RECEIVE_TIMEOUT, + + /// 服务端响应了但是状态码是 400 以上 + RESPONSE, + + /// 请求被取消 + CANCEL, + + /// 没有可用的服务器 + NO_AVAILABLE_HOST, + + /// 已在处理队列中 + IN_PROGRESS, + + /// 未知或者不能处理的错误 + UNKNOWN, +} + +class StorageError extends QiniuError { + /// [type] 不是 [StorageErrorType.RESPONSE] 的时候为 null + final int code; + final StorageErrorType type; + + StorageError({this.type, this.code, Error rawError, String message}) + : super(rawError: rawError, message: message); + + factory StorageError.fromError(Error error) { + return StorageError(type: StorageErrorType.UNKNOWN, rawError: error); + } + + factory StorageError.fromDioError(DioError error) { + return StorageError( + type: _mapDioErrorType(error.type), + code: error.response?.statusCode, + message: error.response?.data.toString(), + rawError: error.error is Error ? (error.error as Error) : null, + ); + } + + @override + String toString() { + var msg = 'StorageError [$type, $code]: $message'; + msg += '\n$stackTrace'; + return msg; + } +} + +StorageErrorType _mapDioErrorType(DioErrorType type) { + switch (type) { + case DioErrorType.CONNECT_TIMEOUT: + return StorageErrorType.CONNECT_TIMEOUT; + case DioErrorType.SEND_TIMEOUT: + return StorageErrorType.SEND_TIMEOUT; + case DioErrorType.RECEIVE_TIMEOUT: + return StorageErrorType.RECEIVE_TIMEOUT; + case DioErrorType.RESPONSE: + return StorageErrorType.RESPONSE; + case DioErrorType.CANCEL: + return StorageErrorType.CANCEL; + case DioErrorType.DEFAULT: + default: + return StorageErrorType.UNKNOWN; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart new file mode 100644 index 00000000..ebcc130c --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart @@ -0,0 +1,20 @@ +part of 'put_parts_task.dart'; + +/// 分片上传用到的缓存 mixin +/// +/// 分片上传的初始化文件、上传分片都应该以此实现缓存控制策略 +mixin CacheMixin on RequestTask { + String get _cacheKey; + + Future clearCache() async { + await config.cacheProvider.removeItem(_cacheKey); + } + + Future setCache(String data) async { + await config.cacheProvider.setItem(_cacheKey, data); + } + + Future getCache() async { + return await config.cacheProvider.getItem(_cacheKey); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart new file mode 100644 index 00000000..46ba3a2d --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart @@ -0,0 +1,51 @@ +part of 'put_parts_task.dart'; + +/// 创建文件,把切片信息合成为一个文件 +class CompletePartsTask extends RequestTask { + final String token; + final String uploadId; + final List parts; + final String key; + + TokenInfo _tokenInfo; + + CompletePartsTask({ + @required this.token, + @required this.uploadId, + @required this.parts, + this.key, + PutController controller, + }) : super(controller: controller); + + @override + void preStart() { + _tokenInfo = Auth.parseUpToken(token); + super.preStart(); + } + + @override + Future createTask() async { + final bucket = _tokenInfo.putPolicy.getBucket(); + + final host = await config.hostProvider.getUpHost( + bucket: bucket, + accessKey: _tokenInfo.accessKey, + ); + final headers = {'Authorization': 'UpToken $token'}; + final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; + final paramUrl = + '$host/buckets/$bucket/objects/$encodedKey/uploads/$uploadId'; + + final response = await client.post>( + paramUrl, + data: { + 'parts': parts + ..sort((a, b) => a.partNumber - b.partNumber) + ..map((part) => part.toJson()).toList() + }, + options: Options(headers: headers), + ); + + return PutResponse.fromJson(response.data); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart new file mode 100644 index 00000000..177b2785 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart @@ -0,0 +1,92 @@ +part of 'put_parts_task.dart'; + +/// initParts 的返回体 +class InitParts { + final int expireAt; + final String uploadId; + + InitParts({ + @required this.expireAt, + @required this.uploadId, + }); + + factory InitParts.fromJson(Map json) { + return InitParts( + uploadId: json['uploadId'] as String, + expireAt: json['expireAt'] as int, + ); + } + + Map toJson() { + return { + 'uploadId': uploadId, + 'expireAt': expireAt, + }; + } +} + +/// 初始化一个分片上传任务,为 [UploadPartsTask] 提供 uploadId +class InitPartsTask extends RequestTask with CacheMixin { + final File file; + final String token; + final String key; + + @override + String _cacheKey; + TokenInfo _tokenInfo; + + InitPartsTask({ + @required this.file, + @required this.token, + this.key, + PutController controller, + }) : super(controller: controller); + + static String getCacheKey(String path, int length, String key) { + return 'qiniu_dart_sdk_init_parts_task_${path}_key_${key}_size_$length'; + } + + @override + void preStart() { + _tokenInfo = Auth.parseUpToken(token); + _cacheKey = InitPartsTask.getCacheKey(file.path, file.lengthSync(), key); + super.preStart(); + } + + @override + Future createTask() async { + final headers = {'Authorization': 'UpToken $token'}; + + final initPartsCache = await getCache(); + if (initPartsCache != null) { + return InitParts.fromJson( + json.decode(initPartsCache) as Map); + } + + final bucket = _tokenInfo.putPolicy.getBucket(); + + final host = await config.hostProvider.getUpHost( + bucket: bucket, + accessKey: _tokenInfo.accessKey, + ); + + final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; + final paramUrl = '$host/buckets/$bucket/objects/$encodedKey/uploads'; + + final response = await client.post>( + paramUrl, + + /// 这里 data 不传,dio 不会触发 cancel 事件 + data: {}, + options: Options(headers: headers), + ); + + return InitParts.fromJson(response.data); + } + + @override + void postReceive(data) async { + await setCache(json.encode(data.toJson())); + super.postReceive(data); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart new file mode 100644 index 00000000..4d5aa881 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart @@ -0,0 +1,26 @@ +part of 'put_parts_task.dart'; + +/// 切片信息 +class Part { + final String etag; + final int partNumber; + + Part({ + @required this.etag, + @required this.partNumber, + }); + + factory Part.fromJson(Map json) { + return Part( + etag: json['etag'] as String, + partNumber: json['partNumber'] as int, + ); + } + + Map toJson() { + return { + 'etag': etag, + 'partNumber': partNumber, + }; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart new file mode 100644 index 00000000..9b1844dd --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart @@ -0,0 +1,25 @@ +import '../put_controller.dart'; + +class PutByPartOptions { + /// 资源名 + /// 如果不传则后端自动生成 + final String key; + + /// 切片大小,单位 MB + /// + /// 超出 [partSize] 的文件大小会把每片按照 [partSize] 的大小切片并上传 + /// 默认 4MB,最小不得小于 1MB,最大不得大于 1024 MB + final int partSize; + + final int maxPartsRequestNumber; + + /// 控制器 + final PutController controller; + + const PutByPartOptions({ + this.key, + this.partSize = 4, + this.maxPartsRequestNumber = 5, + this.controller, + }); +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart new file mode 100644 index 00000000..0cf53fe3 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart @@ -0,0 +1,187 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; + +part 'cache_mixin.dart'; +part 'complete_parts_task.dart'; +part 'init_parts_task.dart'; +part 'part.dart'; +part 'upload_part_task.dart'; +part 'upload_parts_task.dart'; + +/// 分片上传任务 +class PutByPartTask extends RequestTask { + final File file; + final String token; + + final int partSize; + final int maxPartsRequestNumber; + + final String key; + + /// 设置为 0,避免子任务重试失败后 [PutByPartTask] 继续重试 + @override + int get retryLimit => 0; + + PutByPartTask({ + @required this.file, + @required this.token, + @required this.partSize, + @required this.maxPartsRequestNumber, + this.key, + PutController controller, + }) : assert(file != null), + assert(token != null), + assert(partSize != null), + assert(maxPartsRequestNumber != null), + assert(() { + if (partSize < 1 || partSize > 1024) { + throw RangeError.range(partSize, 1, 1024, 'partSize', + 'partSize must be greater than 1 and less than 1024'); + } + return true; + }()), + super(controller: controller); + + RequestTaskController _currentWorkingTaskController; + + @override + void preStart() { + super.preStart(); + + // 处理相同任务 + final sameTaskExsist = manager.getTasks().firstWhere( + (element) => element is PutByPartTask && isEquals(element), + orElse: () => null, + ); + + if (sameTaskExsist != null) { + throw StorageError( + type: StorageErrorType.IN_PROGRESS, + message: '$file 已在上传队列中', + ); + } + + // controller 被取消后取消当前运行的子任务 + controller?.cancelToken?.whenCancel?.then((_) { + _currentWorkingTaskController?.cancel(); + }); + } + + @override + void postReceive(PutResponse data) { + _currentWorkingTaskController = null; + super.postReceive(data); + } + + @override + Future createTask() async { + controller?.notifyStatusListeners(StorageStatus.Request); + + final initPartsTask = _createInitParts(); + final initParts = await initPartsTask.future; + + // 初始化任务完成后也告诉外部一个进度 + controller?.notifyProgressListeners(0.002); + + final uploadParts = _createUploadParts(initParts.uploadId); + + PutResponse putResponse; + try { + final parts = await uploadParts.future; + putResponse = + await _createCompleteParts(initParts.uploadId, parts).future; + } catch (error) { + // 拿不到 initPartsTask 和 uploadParts 的引用,所以不放到 postError 去 + if (error is StorageError) { + /// 满足以下两种情况清理缓存: + /// 1、如果服务端文件被删除了,清除本地缓存 + /// 2、如果 uploadId 等参数不对原因会导致 400 + if (error.code == 612 || error.code == 400) { + await initPartsTask.clearCache(); + await uploadParts.clearCache(); + } + + /// 如果服务端文件被删除了,重新上传 + if (error.code == 612) { + controller?.notifyStatusListeners(StorageStatus.Retry); + return createTask(); + } + } + + rethrow; + } + + /// 上传完成,清除缓存 + await initPartsTask.clearCache(); + await uploadParts.clearCache(); + + return putResponse; + } + + bool isEquals(PutByPartTask target) { + return target.file.path == file.path && + target.key == key && + target.file.lengthSync() == file.lengthSync(); + } + + /// 初始化上传信息,分片上传的第一步 + InitPartsTask _createInitParts() { + final _controller = PutController(); + + final task = InitPartsTask( + file: file, + token: token, + key: key, + controller: _controller, + ); + + manager.addTask(task); + _currentWorkingTaskController = _controller; + return task; + } + + UploadPartsTask _createUploadParts(String uploadId) { + final _controller = PutController(); + + final task = UploadPartsTask( + file: file, + token: token, + partSize: partSize, + uploadId: uploadId, + maxPartsRequestNumber: maxPartsRequestNumber, + key: key, + controller: _controller, + ); + + _controller.addSendProgressListener(onSendProgress); + + manager.addTask(task); + _currentWorkingTaskController = _controller; + return task; + } + + /// 创建文件,分片上传的最后一步 + CompletePartsTask _createCompleteParts( + String uploadId, + List parts, + ) { + final _controller = PutController(); + final task = CompletePartsTask( + token: token, + uploadId: uploadId, + parts: parts, + key: key, + controller: _controller, + ); + + manager.addTask(task); + _currentWorkingTaskController = _controller; + return task; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart new file mode 100644 index 00000000..4c615963 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart @@ -0,0 +1,112 @@ +part of 'put_parts_task.dart'; + +// 上传一个 part 的任务 +class UploadPartTask extends RequestTask { + final String token; + final String uploadId; + final RandomAccessFile raf; + final int partSize; + + // 如果 data 是 Stream 的话,Dio 需要判断 content-length 才会调用 onSendProgress + // https://github.com/flutterchina/dio/blob/21136168ab39a7536835c7a59ce0465bb05feed4/dio/lib/src/dio.dart#L1000 + final int byteLength; + + final int partNumber; + + final String key; + + TokenInfo _tokenInfo; + + UploadPartTask({ + @required this.token, + @required this.raf, + @required this.uploadId, + @required this.byteLength, + @required this.partNumber, + @required this.partSize, + this.key, + PutController controller, + }) : super(controller: controller); + + @override + void preStart() { + _tokenInfo = Auth.parseUpToken(token); + super.preStart(); + } + + @override + void postReceive(data) { + controller?.notifyProgressListeners(1); + super.postReceive(data); + } + + @override + Future createTask() async { + final headers = { + 'Authorization': 'UpToken $token', + Headers.contentLengthHeader: byteLength, + }; + + final bucket = _tokenInfo.putPolicy.getBucket(); + + final host = await config.hostProvider.getUpHost( + bucket: bucket, + accessKey: _tokenInfo.accessKey, + ); + + final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; + final paramUrl = 'buckets/$bucket/objects/$encodedKey'; + + final response = await client.put>( + '$host/$paramUrl/uploads/$uploadId/$partNumber', + data: Stream.fromIterable([_readFileByPartNumber(partNumber)]), + // 在 data 是 stream 的场景下, interceptor 传入 cancelToken 这里不传会有 bug + cancelToken: controller.cancelToken, + options: Options(headers: headers), + ); + + return UploadPart.fromJson(response.data); + } + + // 分片上传是手动从 File 拿一段数据大概 4m(直穿是直接从 File 里面读取) + // 如果文件是 21m,假设切片是 4 * 5 + // 外部进度的话会导致一下长到 90% 多,然后变成 100% + // 解决方法是覆盖父类的 onSendProgress,让 onSendProgress 不处理 Progress 的进度 + // 改为发送成功后通知(见 postReceive) + @override + void onSendProgress(double percent) { + controller?.notifySendProgressListeners(percent); + } + + // 根据 partNumber 获取要上传的文件片段 + List _readFileByPartNumber(int partNumber) { + final startOffset = (partNumber - 1) * partSize * 1024 * 1024; + raf.setPositionSync(startOffset); + return raf.readSync(byteLength); + } +} + +// uploadPart 的返回体 +class UploadPart { + final String md5; + final String etag; + + UploadPart({ + @required this.md5, + @required this.etag, + }); + + factory UploadPart.fromJson(Map json) { + return UploadPart( + md5: json['md5'] as String, + etag: json['etag'] as String, + ); + } + + Map toJson() { + return { + 'etag': etag, + 'md5': md5, + }; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart new file mode 100644 index 00000000..a42ca87a --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart @@ -0,0 +1,260 @@ +part of 'put_parts_task.dart'; + +// 批处理上传 parts 的任务,为 [CompletePartsTask] 提供 [Part] +class UploadPartsTask extends RequestTask> with CacheMixin { + final File file; + final String token; + final String uploadId; + + final int partSize; + final int maxPartsRequestNumber; + + final String key; + + @override + String _cacheKey; + + /// 设置为 0,避免子任务重试失败后 [UploadPartsTask] 继续重试 + @override + int get retryLimit => 0; + + // 文件 bytes 长度 + int _fileByteLength; + + // 每个上传分片的字节长度 + // + // 文件会按照此长度切片 + int _partByteLength; + + // 文件总共被拆分的分片数 + int _totalPartCount; + + // 上传成功后把 part 信息存起来 + final Map _uploadedPartMap = {}; + + // 处理分片上传任务的 UploadPartTask 的控制器 + final List _workingUploadPartTaskControllers = []; + + // 已发送分片数量 + int _sentPartCount = 0; + + // 已发送到服务器的数量 + int _sentPartToServerCount = 0; + + // 剩余多少被允许的请求数 + int _idleRequestNumber; + + RandomAccessFile _raf; + + UploadPartsTask({ + @required this.file, + @required this.token, + @required this.uploadId, + @required this.partSize, + @required this.maxPartsRequestNumber, + this.key, + PutController controller, + }) : super(controller: controller); + + static String getCacheKey( + String path, + int length, + int partSize, + String key, + ) { + final keyList = [ + 'key/$key', + 'path/$path', + 'file_size/$length', + 'part_size/$partSize', + ]; + + return 'qiniu_dart_sdk_upload_parts_task@[${keyList..join("/")}]'; + } + + @override + void preStart() { + // 当前 controller 被取消后,所有运行中的子任务都需要被取消 + controller?.cancelToken?.whenCancel?.then((_) { + for (final controller in _workingUploadPartTaskControllers) { + controller.cancel(); + } + }); + _fileByteLength = file.lengthSync(); + _partByteLength = partSize * 1024 * 1024; + _idleRequestNumber = maxPartsRequestNumber; + _totalPartCount = (_fileByteLength / _partByteLength).ceil(); + _cacheKey = getCacheKey(file.path, _fileByteLength, partSize, key); + // 子任务 UploadPartTask 从 file 去 open 的话虽然上传精度会颗粒更细但是会导致可能读不出文件的问题 + // 可能 close 没办法立即关闭 file stream,而延迟 close 了,导致某次 open 的 stream 被立即关闭 + // 所以读不出内容了 + // 这里改成这里读取一次,子任务从中读取 bytes + _raf = file.openSync(); + super.preStart(); + } + + @override + void postReceive(data) async { + await _raf.close(); + super.postReceive(data); + } + + @override + void postError(Object error) async { + await _raf.close(); + // 取消,网络问题等可能导致上传中断,缓存已上传的分片信息 + await storeUploadedPart(); + super.postError(error); + } + + Future storeUploadedPart() async { + if (_uploadedPartMap.isEmpty) { + return; + } + + await setCache(jsonEncode(_uploadedPartMap.values.toList())); + } + + // 从缓存恢复已经上传的 part + Future recoverUploadedPart() async { + // 获取缓存 + final cachedData = await getCache(); + // 尝试从缓存恢复 + if (cachedData != null) { + var cachedList = []; + + try { + final _cachedList = json.decode(cachedData) as List; + cachedList = _cachedList + .map((dynamic item) => Part.fromJson(item as Map)) + .toList(); + } catch (error) { + rethrow; + } + + for (final part in cachedList) { + _uploadedPartMap[part.partNumber] = part; + } + } + } + + @override + Future> createTask() async { + /// 如果已经取消了,直接报错 + // ignore: null_aware_in_condition + if (controller != null && controller.cancelToken.isCancelled) { + throw StorageError(type: StorageErrorType.CANCEL); + } + + controller.notifyStatusListeners(StorageStatus.Request); + // 尝试恢复缓存,如果有 + await recoverUploadedPart(); + + // 上传分片 + await _uploadParts(); + return _uploadedPartMap.values.toList(); + } + + int _uploadingPartIndex = 0; + + // 从指定的分片位置往后上传切片 + Future _uploadParts() async { + final tasksLength = + min(_idleRequestNumber, _totalPartCount - _uploadingPartIndex); + final taskFutures = >[]; + + while (taskFutures.length < tasksLength && + _uploadingPartIndex < _totalPartCount) { + // partNumber 按照后端要求必须从 1 开始 + final partNumber = ++_uploadingPartIndex; + + final _uploadedPart = _uploadedPartMap[partNumber]; + if (_uploadedPart != null) { + _sentPartCount++; + _sentPartToServerCount++; + notifySendProgress(); + notifyProgress(); + continue; + } + + final future = _createUploadPartTaskFutureByPartNumber(partNumber); + taskFutures.add(future); + } + + await Future.wait(taskFutures); + } + + Future _createUploadPartTaskFutureByPartNumber(int partNumber) async { + // 上传分片(part)的字节大小 + final _byteLength = _getPartSizeByPartNumber(partNumber); + + _idleRequestNumber--; + final _controller = PutController(); + _workingUploadPartTaskControllers.add(_controller); + + final task = UploadPartTask( + token: token, + raf: _raf, + uploadId: uploadId, + byteLength: _byteLength, + partNumber: partNumber, + partSize: partSize, + key: key, + controller: _controller, + ); + + _controller + // UploadPartTask 一次上传一个 chunk,通知一次进度 + ..addSendProgressListener((percent) { + _sentPartCount++; + notifySendProgress(); + }) + // UploadPartTask 上传完成后触发 + ..addProgressListener((percent) { + _sentPartToServerCount++; + notifyProgress(); + }); + + manager.addTask(task); + + final data = await task.future; + + _idleRequestNumber++; + _uploadedPartMap[partNumber] = + Part(partNumber: partNumber, etag: data.etag); + _workingUploadPartTaskControllers.remove(_controller); + + await storeUploadedPart(); + + // 检查任务是否已经完成 + if (_uploadedPartMap.length != _totalPartCount) { + // 上传下一片 + await _uploadParts(); + } + } + + // 根据 partNumber 算出当前切片的 byte 大小 + int _getPartSizeByPartNumber(int partNumber) { + final startOffset = (partNumber - 1) * _partByteLength; + + if (partNumber == _totalPartCount) { + return _fileByteLength - startOffset; + } + + return _partByteLength; + } + + void notifySendProgress() { + controller?.notifySendProgressListeners(_sentPartCount / _totalPartCount); + } + + void notifyProgress() { + controller?.notifyProgressListeners(_sentPartToServerCount / + _totalPartCount * + RequestTask.onSendProgressTakePercentOfTotal); + } + + // UploadPartsTask 自身不包含进度,在其他地方处理 + @override + void onSendProgress(double percent) {} +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart new file mode 100644 index 00000000..00e34526 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart @@ -0,0 +1,13 @@ +import '../put_controller.dart'; + +class PutBySingleOptions { + /// 资源名 + /// + /// 如果不传则后端自动生成 + final String key; + + /// 控制器 + final PutController controller; + + const PutBySingleOptions({this.key, this.controller}); +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart new file mode 100644 index 00000000..4917a7ad --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:qiniu_sdk_base/src/storage/task/task.dart'; + +import '../../../../auth/auth.dart'; +import '../put_response.dart'; + +// 直传任务 +class PutBySingleTask extends RequestTask { + /// 上传文件 + final File file; + + /// 上传凭证 + final String token; + + /// 资源名 + /// 如果不传则后端自动生成 + final String key; + + TokenInfo _tokenInfo; + + PutBySingleTask({ + @required this.file, + @required this.token, + this.key, + RequestTaskController controller, + }) : assert(file != null), + assert(token != null), + super(controller: controller); + + @override + void preStart() { + _tokenInfo = Auth.parseUpToken(token); + super.preStart(); + } + + @override + Future createTask() async { + final formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(file.path), + 'token': token, + 'key': key, + }); + + final host = await config.hostProvider.getUpHost( + accessKey: _tokenInfo.accessKey, + bucket: _tokenInfo.putPolicy.getBucket(), + ); + + final response = await client.post>( + host, + data: formData, + cancelToken: controller?.cancelToken, + ); + + return PutResponse.fromJson(response.data); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart new file mode 100644 index 00000000..af6273fb --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart @@ -0,0 +1,5 @@ +export 'by_part/put_by_part_options.dart'; +export 'by_single/put_by_single_options.dart'; +export 'put_controller.dart'; +export 'put_options.dart'; +export 'put_response.dart'; diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart new file mode 100644 index 00000000..84026531 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart @@ -0,0 +1,3 @@ +import '../../task/request_task.dart'; + +class PutController extends RequestTaskController {} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart new file mode 100644 index 00000000..1896a0ac --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart @@ -0,0 +1,28 @@ +import 'put_controller.dart'; + +class PutOptions { + /// 资源名 + /// + /// 如果不传则后端自动生成 + final String key; + + /// 强制使用单文件上传,不使用分片,默认值 false + final bool forceBySingle; + + /// 使用分片上传时的分片大小,默认值 4,单位为 MB + final int partSize; + + /// 并发上传的队列长度,默认值为 5 + final int maxPartsRequestNumber; + + /// 控制器 + final PutController controller; + + const PutOptions({ + this.key, + this.forceBySingle = false, + this.partSize = 4, + this.maxPartsRequestNumber = 5, + this.controller, + }); +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_response.dart b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_response.dart new file mode 100644 index 00000000..bd9ca5a2 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/methods/put/put_response.dart @@ -0,0 +1,33 @@ +import 'package:meta/meta.dart'; + +class PutResponse { + final String key; + final String hash; + + /// 如果在上传策略自定义了 [returnBody], + /// 你可以读取并解析这个字段提取你自定义的响应信息 + final Map rawData; + + PutResponse({ + @required this.key, + @required this.hash, + @required this.rawData, + }); + + factory PutResponse.fromJson(Map json) { + return PutResponse( + key: json['key'] as String, + hash: json['hash'] as String, + rawData: json, + ); + } + + Map toJson() { + return { + 'key': key, + 'hash': hash, + 'rawData': rawData + }; + } + +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/status/status.dart b/qiniu-dart-sdk/base/lib/src/storage/status/status.dart new file mode 100644 index 00000000..b7fdcf6d --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/status/status.dart @@ -0,0 +1,21 @@ +enum StorageStatus { + None, + + /// 初始化任务 + Init, + + /// 请求准备发出的时候触发 + Request, + + /// 请求完成后触发 + Success, + + /// 请求被取消后触发 + Cancel, + + /// 请求出错后触发 + Error, + + /// 请求出错触发重试时触发 + Retry +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/storage.dart b/qiniu-dart-sdk/base/lib/src/storage/storage.dart new file mode 100644 index 00000000..8b8d98d5 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/storage.dart @@ -0,0 +1,99 @@ +import 'dart:io'; + +import 'config/config.dart'; +import 'methods/put/by_part/put_parts_task.dart'; +import 'methods/put/by_single/put_by_single_task.dart'; +import 'methods/put/put.dart'; +import 'task/task.dart'; + +export 'package:dio/dio.dart' show HttpClientAdapter; +export 'config/config.dart'; +export 'error/error.dart'; +export 'methods/put/put.dart'; +export 'status/status.dart'; +export 'task/request_task.dart'; +export 'task/task.dart'; + +/// 客户端 +class Storage { + Config config; + RequestTaskManager taskManager; + + Storage({Config config}) { + this.config = config ?? Config(); + taskManager = RequestTaskManager(config: this.config); + } + + Future putFile( + File file, + String token, { + PutOptions options, + }) { + options ??= PutOptions(); + RequestTask task; + final useSingle = options.forceBySingle == true || + file.lengthSync() < (options.partSize * 1024 * 1024); + + if (useSingle) { + task = PutBySingleTask( + file: file, + token: token, + key: options.key, + controller: options.controller, + ); + } else { + task = PutByPartTask( + file: file, + token: token, + key: options.key, + maxPartsRequestNumber: options.maxPartsRequestNumber, + partSize: options.partSize, + controller: options.controller, + ); + } + + taskManager.addTask(task); + + return task.future; + } + + /// 单文件上传 + Future putFileBySingle( + File file, + String token, { + PutBySingleOptions options, + }) { + options ??= PutBySingleOptions(); + final task = PutBySingleTask( + file: file, + token: token, + key: options.key, + controller: options.controller, + ); + + taskManager.addTask(task); + + return task.future; + } + + /// 分片上传 + Future putFileByPart( + File file, + String token, { + PutByPartOptions options, + }) { + options ??= PutByPartOptions(); + final task = PutByPartTask( + file: file, + token: token, + key: options.key, + partSize: options.partSize, + maxPartsRequestNumber: options.maxPartsRequestNumber, + controller: options.controller, + ); + + taskManager.addTask(task); + + return task.future; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/task/request_task.dart b/qiniu-dart-sdk/base/lib/src/storage/task/request_task.dart new file mode 100644 index 00000000..501a720f --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/task/request_task.dart @@ -0,0 +1,207 @@ +import 'dart:io' show Platform; +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; + +import 'task.dart'; + +part 'request_task_controller.dart'; +part 'request_task_manager.dart'; + +String _getUserAgent() { + return [ + '${Platform.operatingSystem}/${Platform.operatingSystemVersion}', + 'Dart/${Platform.version}' + ].join(' '); +} + +abstract class RequestTask extends Task { + // 准备阶段占总任务的百分比 + static double preStartTakePercentOfTotal = 0.001; + // 处理中阶段占总任务的百分比 + static double onSendProgressTakePercentOfTotal = 0.99; + // 完成阶段占总任务的百分比 + static double postReceiveTakePercentOfTotal = 1; + + final Dio client = Dio(); + + /// [RequestTaskManager.addTask] 会初始化这个 + Config config; + @override + // ignore: overridden_fields + covariant RequestTaskManager manager; + RequestTaskController controller; + + // 重试次数 + int _retryCount = 0; + + // 最大重试次数 + int retryLimit; + + RequestTask({this.controller}); + + @override + @mustCallSuper + void preStart() { + // 如果已经取消了,直接报错 + if (controller != null && controller.cancelToken.isCancelled) { + throw StorageError(type: StorageErrorType.CANCEL); + } + controller?.notifyStatusListeners(StorageStatus.Init); + controller?.notifyProgressListeners(preStartTakePercentOfTotal); + retryLimit = config.retryLimit; + client.httpClientAdapter = config.httpClientAdapter; + client.interceptors.add(InterceptorsWrapper(onRequest: (options) { + controller?.notifyStatusListeners(StorageStatus.Request); + options + ..cancelToken = controller?.cancelToken + ..onSendProgress = (sent, total) => onSendProgress(sent / total); + + return options; + })); + + client.interceptors.add(InterceptorsWrapper(onRequest: (options) { + options.headers['User-Agent'] = _getUserAgent(); + return options; + })); + + super.preStart(); + } + + @override + @mustCallSuper + void preRestart() { + controller?.notifyStatusListeners(StorageStatus.Retry); + super.preRestart(); + } + + @override + @mustCallSuper + void postReceive(T data) { + controller?.notifyStatusListeners(StorageStatus.Success); + controller?.notifyProgressListeners(postReceiveTakePercentOfTotal); + super.postReceive(data); + } + + /// [createTask] 被取消后触发 + @mustCallSuper + void postCancel(StorageError error) { + controller?.notifyStatusListeners(StorageStatus.Cancel); + } + + @override + @mustCallSuper + void postError(Object error) async { + // 处理 Dio 异常 + if (error is DioError) { + if (!_canConnectToHost(error)) { + // host 连不上,判断是否 host 不可用造成的, 比如 tls error(没做还) + if (_isHostUnavailable(error)) { + config.hostProvider.freezeHost(error.request.path); + } + + // 继续尝试当前 host,如果是服务器坏了则切换到其他 host + if (_retryCount < retryLimit) { + _retryCount++; + manager.restartTask(this); + return; + } + } + + // 能连上但是服务器不可用,比如 502 + if (_isHostUnavailable(error)) { + config.hostProvider.freezeHost(error.request.path); + + // 切换到其他 host + if (_retryCount < retryLimit) { + _retryCount++; + manager.restartTask(this); + return; + } + } + + final storageError = StorageError.fromDioError(error); + + // 通知状态 + if (error.type == DioErrorType.CANCEL) { + postCancel(storageError); + } else { + controller?.notifyStatusListeners(StorageStatus.Error); + } + + super.postError(storageError); + return; + } + + // 处理 Storage 异常。如果有子任务,错误可能被子任务加工成 StorageError + if (error is StorageError) { + if (error.type == StorageErrorType.CANCEL) { + postCancel(error); + } else { + controller?.notifyStatusListeners(StorageStatus.Error); + } + + super.postError(error); + return; + } + + // 不能处理的异常 + if (error is Error) { + controller?.notifyStatusListeners(StorageStatus.Error); + final storageError = StorageError.fromError(error); + super.postError(storageError); + return; + } + + controller?.notifyStatusListeners(StorageStatus.Error); + super.postError(error); + } + + // 自定义发送进度处理逻辑 + void onSendProgress(double percent) { + controller?.notifySendProgressListeners(percent); + controller + ?.notifyProgressListeners(percent * onSendProgressTakePercentOfTotal); + } + + // host 是否可以连接上 + bool _canConnectToHost(Object error) { + if (error is DioError) { + if (error.type == DioErrorType.RESPONSE && + error.response.statusCode > 99) { + return true; + } + + if (error.type == DioErrorType.CANCEL) { + return true; + } + } + + return false; + } + + // host 是否不可用 + bool _isHostUnavailable(Object error) { + if (error is DioError) { + if (error.type == DioErrorType.RESPONSE) { + final statusCode = error.response.statusCode; + if (statusCode == 502) { + return true; + } + if (statusCode == 503) { + return true; + } + if (statusCode == 504) { + return true; + } + if (statusCode == 599) { + return true; + } + } + // ignore: todo + // TODO 更详细的信息 SocketException + } + + return false; + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/task/request_task_controller.dart b/qiniu-dart-sdk/base/lib/src/storage/task/request_task_controller.dart new file mode 100644 index 00000000..d908a463 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/task/request_task_controller.dart @@ -0,0 +1,97 @@ +part of 'request_task.dart'; + +class RequestTaskController + with + RequestTaskProgressListenersMixin, + StorageStatusListenersMixin, + RequestTaskSendProgressListenersMixin { + final CancelToken cancelToken = CancelToken(); + + /// 是否被取消过 + bool get isCancelled => cancelToken.isCancelled; + + void cancel() { + // 允许重复取消,但是已经取消后不会有任何行为发生 + if (isCancelled) { + return; + } + + cancelToken.cancel(); + } +} + +typedef RequestTaskSendProgressListener = void Function(double percent); + +/// 请求发送进度 +/// +/// 使用 Dio 发出去的请求才会触发 +mixin RequestTaskSendProgressListenersMixin { + final List _sendProgressListeners = []; + + void Function() addSendProgressListener( + RequestTaskSendProgressListener listener) { + _sendProgressListeners.add(listener); + return () => removeSendProgressListener(listener); + } + + void removeSendProgressListener(RequestTaskSendProgressListener listener) { + _sendProgressListeners.remove(listener); + } + + void notifySendProgressListeners(double percent) { + for (final listener in _sendProgressListeners) { + listener(percent); + } + } +} + +typedef RequestTaskProgressListener = void Function(double percent); + +/// 任务进度 +/// +/// 当前任务的总体进度,初始化占 1%,处理请求占 98%,完成占 1%,总体 100% +mixin RequestTaskProgressListenersMixin { + final List _progressListeners = []; + + void Function() addProgressListener(RequestTaskProgressListener listener) { + _progressListeners.add(listener); + return () => removeProgressListener(listener); + } + + void removeProgressListener(RequestTaskProgressListener listener) { + _progressListeners.remove(listener); + } + + void notifyProgressListeners(double percent) { + for (final listener in _progressListeners) { + listener(percent); + } + } +} + +typedef StorageStatusListener = void Function(StorageStatus status); + +/// 任务状态。 +/// +/// 自动触发(preStart, postReceive) +mixin StorageStatusListenersMixin { + StorageStatus status = StorageStatus.None; + + final List _statusListeners = []; + + void Function() addStatusListener(StorageStatusListener listener) { + _statusListeners.add(listener); + return () => removeStatusListener(listener); + } + + void removeStatusListener(StorageStatusListener listener) { + _statusListeners.remove(listener); + } + + void notifyStatusListeners(StorageStatus status) { + status = status; + for (final listener in _statusListeners) { + listener(status); + } + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/task/request_task_manager.dart b/qiniu-dart-sdk/base/lib/src/storage/task/request_task_manager.dart new file mode 100644 index 00000000..c3fd03e2 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/task/request_task_manager.dart @@ -0,0 +1,15 @@ +part of 'request_task.dart'; + +class RequestTaskManager extends TaskManager { + final Config config; + + RequestTaskManager({ + @required this.config, + }) : assert(config != null); + + @override + void addTask(covariant RequestTask task) { + task.config = config; + super.addTask(task); + } +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/task/task.dart b/qiniu-dart-sdk/base/lib/src/storage/task/task.dart new file mode 100644 index 00000000..9cb6b607 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/task/task.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; + +export 'request_task.dart'; +export 'task_manager.dart'; + +/// 定义一个 Task 的抽象类 +/// +/// 异步的任务,比如请求,批处理都可以继承这个类实现一个 Task +abstract class Task { + TaskManager manager; + + @protected + Completer completer = Completer(); + + Future get future => completer.future; + + /// 创建任务的抽象方法 + Future createTask(); + + /// [Task] 启动之前会调用,该方法只会在第一次被 [TaskManager] 初始化的时候调用 + @mustCallSuper + void preStart() {} + + /// [createTask] 执行之后会调用 + @mustCallSuper + void postStart() {} + + /// 在 [createTask] 的返回值接收到结果之后调用 + @mustCallSuper + void postReceive(T data) { + manager.removeTask(this); + completer.complete(data); + } + + /// 在 [createTask] 的返回值出错之后调用 + @mustCallSuper + void postError(Object error) { + manager.removeTask(this); + completer.completeError(error); + } + + /// Task 被重启之前执行,[Task.restart] 调用后立即执行 + void preRestart() {} + + /// Task 被重启之后执行,[createTask] 被重新调用后执行 + void postRestart() {} +} diff --git a/qiniu-dart-sdk/base/lib/src/storage/task/task_manager.dart b/qiniu-dart-sdk/base/lib/src/storage/task/task_manager.dart new file mode 100644 index 00000000..78f743b0 --- /dev/null +++ b/qiniu-dart-sdk/base/lib/src/storage/task/task_manager.dart @@ -0,0 +1,66 @@ +import 'package:meta/meta.dart'; + +import 'task.dart'; + +class TaskManager { + final List workingTasks = []; + + /// 添加一个 [Task] + /// + /// 被添加的 [task] 会被立即执行 [createTask] + @mustCallSuper + void addTask(Task task) { + try { + task + ..manager = this + ..preStart(); + } catch (e) { + task.postError(e); + return; + } + + workingTasks.add(task); + task.createTask().then(task.postReceive).catchError(task.postError); + + try { + task.postStart(); + } catch (e) { + task.postError(e); + return; + } + } + + @mustCallSuper + void removeTask(Task task) { + workingTasks.remove(task); + } + + @mustCallSuper + void restartTask(Task task) { + try { + task.preRestart(); + } catch (e) { + task.postError(e); + return; + } + + task.createTask().then(task.postReceive).catchError(task.postError); + + try { + task.postRestart(); + } catch (e) { + task.postError(e); + return; + } + } + + /// 返回当前运行中的 [Task] + List> getTasks() { + return workingTasks; + } + + /// 查找类型符合 [T] 的 [Task] + List getTasksByType>() { + return workingTasks.whereType().toList(); + } +} diff --git a/qiniu-dart-sdk/base/pubspec.lock b/qiniu-dart-sdk/base/pubspec.lock new file mode 100644 index 00000000..4bb4df6e --- /dev/null +++ b/qiniu-dart-sdk/base/pubspec.lock @@ -0,0 +1,461 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4d2a2c7f5e6db6acd2f4a7f713b308b45668c9573a11d0eda10936fb21fc5467" + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "9fe5033adc3e8b19884e5ba296400be3794483acb53c2b68f8683744c9a7a9c7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.41.2" + args: + dependency: transitive + description: + name: args + sha256: "6ba785824030bc97154264652acfd6a2dc699cd85f6def708fb7534d23ef1348" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "3fa83bc417e26f1d55814443a603747d48e3cdf55b2f4fea27dea8e9224dcefd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "7c2065a21e40c96eb0674205a48616dc4dcff138c78dfc8f46666d65b7512a46" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.14.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: "3ce628f3c6a7144be6c524dbb20ca5af52fa59c850c587bf5e52cd70ece8e7e8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + dart2_constant: + dependency: transitive + description: + name: dart2_constant + sha256: d9791e9e1f43ab97909333f37562df8e383a906940f44700de77dd0b0a081b18 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2+dart2" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11979099d9ea182d74b6734340704d628b99c7a8316f9edd7718a297d1bcdd27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + dotenv: + dependency: "direct dev" + description: + name: dotenv + sha256: "7a906a39c0ec36c0427d5ddcf9e78004f28ffcd61be4ca5db56a823cb15e0ad2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + sha256: "781811e896666e9a728fd85b13129e32d73f05167a5da1891739f4609c6a8d7d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" + glob: + dependency: transitive + description: + name: glob + sha256: f699efc018c8783b7955c74c13b1952811feb38fb6fe13f9546752a6573dde5f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "9d2b0626e9e402fc98e6868360da8f256064d6c0b8e4c3edcca5e02fb0b95da9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "6cec7404b25d6338c8cb7b30131cd6c760079a4ec1fa7846c55bdda91f9d2819" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + lcov: + dependency: transitive + description: + name: lcov + sha256: "36e28d3face9c62daf00bf6861fcd6fdb579ca09a5280cbb3ac36c5b073c615e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: "239402c239e2a390cf745af3a496fb985dc8f0b31b3f5fe0d460e42289d33ad7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: "38c7be344ac5057e10161a5ecb00c9d9d67ed2f150001278601dd27d9fe64206" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.10" + meta: + dependency: "direct main" + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: a7a98ea7f366e2cc9d2b20873815aebec5e2bc124fe0da9d3f7f59b0625ea180 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + node_interop: + dependency: transitive + description: + name: node_interop + sha256: "910d75f8589630cd197b8b7ec70e648bc380950b489b07bda1da0c5b20c4b8c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + node_io: + dependency: transitive + description: + name: node_io + sha256: "36b74b2bcea9aa7484caa91e24bddde3f56e87fdc84f4da1cab7522a4b3680fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "847d2400938dd2594b4617244c4a9d477e939d0e134746ae88d9497e0ecb0a71" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.13" + package_config: + dependency: transitive + description: + name: package_config + sha256: "08b3acb5f8c9736aaf26776864ea68441bb69a82d900cbce47cd962b3ddb12c9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.3" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.3" + pedantic: + dependency: "direct dev" + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: d16d1197f877e814f767be03c1c5940a397ae0ddb46dcfb9b5465074bb80ee37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.9" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "0947da47a2d858bcf2cdf87986098808488b9c509562f55d9748abffc5cd024e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: e5ddf2266fa3d342728476a4c9718735d1e8eff1c5da1f8a3df0042f3b83c83c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.9+2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "2a300f6dd3d4a6d41941eef97920671ecd250712ce1275bf60110997691b20d1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.4+1" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "53e45e20eed3f8859b3af1cb1c913a1667258d727423f46027df9a3f88822435" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.5" + test_api: + dependency: transitive + description: + name: test_api + sha256: "637a0c7ff81307fe6e31edfd338c798aeeef4c2c3fad031fc91ec92d5ef47c75" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.19" + test_core: + dependency: transitive + description: + name: test_core + sha256: "6442993591fc131006752949b0dfaaa9650243f615a71d4dfa33743ea2709767" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.15" + test_coverage: + dependency: "direct dev" + description: + name: test_coverage + sha256: "4c38230a26f8b7223de0a14cb0ff2257087d6f824091fa6572a472ee8bff3cb7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ec8861f85bf8037ebdb453066879ece9d0f25fdac42cd2ff4d43b47239d0f8aa + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.5.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: bfd93faab893a64920903d7d375517c18789c21e4c7daa5232a31ee126b7fea1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "750cd8919c8ce82d399de5092f5dfce5dd295a4e52fa785540b6dbca0cef94d7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.5" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" +sdks: + dart: ">=2.19.0 <3.0.0" diff --git a/qiniu-dart-sdk/base/pubspec.yaml b/qiniu-dart-sdk/base/pubspec.yaml new file mode 100644 index 00000000..49d39752 --- /dev/null +++ b/qiniu-dart-sdk/base/pubspec.yaml @@ -0,0 +1,18 @@ +name: qiniu_sdk_base +version: 0.2.2 +homepage: https://github.com/qiniu/dart-sdk +description: The sdk basic of Qiniu products + +environment: + sdk: '>=2.7.0 <3.0.0' + +dependencies: + dio: ^3.0.10 + crypto: ^2.1.5 + meta: ^1.2.3 + +dev_dependencies: + pedantic: ^1.9.0 + test: ^1.14.4 + dotenv: ^1.0.0 + test_coverage: "^0.5.0" diff --git a/qiniu-dart-sdk/codecov.yml b/qiniu-dart-sdk/codecov.yml new file mode 100644 index 00000000..7e366012 --- /dev/null +++ b/qiniu-dart-sdk/codecov.yml @@ -0,0 +1,11 @@ +coverage: + status: + # 提 pr 的时候不让 codecov 评论哪里没覆盖到 + patch: off + project: + default: + # pr 里面很多测试用例都被跳过了,所以这个 target 没有意义。如果能让需要 token 的测试在 pr 跑起来可以修改这个 target + target: 0% + branches: + - master + informational: true diff --git a/qiniu-dart-sdk/flutter/CHANGELOG.md b/qiniu-dart-sdk/flutter/CHANGELOG.md new file mode 100644 index 00000000..8bce45ff --- /dev/null +++ b/qiniu-dart-sdk/flutter/CHANGELOG.md @@ -0,0 +1,8 @@ +## [0.1.0] + +* Initial Release. + +## [0.2.0] + +* 优化了 `StorageError` 输出的调用栈 +* `CacheProvider` 的方法都改成异步的 diff --git a/qiniu-dart-sdk/flutter/LICENSE b/qiniu-dart-sdk/flutter/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/qiniu-dart-sdk/flutter/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/qiniu-dart-sdk/flutter/README.md b/qiniu-dart-sdk/flutter/README.md new file mode 100644 index 00000000..66b93b04 --- /dev/null +++ b/qiniu-dart-sdk/flutter/README.md @@ -0,0 +1,182 @@ +# 七牛云存储 Flutter SDK [![qiniu_flutter_sdk](https://img.shields.io/pub/v/qiniu_flutter_sdk.svg?label=qiniu_flutter_sdk)](https://pub.dev/packages/qiniu_flutter_sdk) + +七牛云存储的 Flutter SDK。 +基于七牛云 API 实现,封装了七牛云存储系统的的客户端操作。 + +## 快速导航 + +* [概述](#概述) +* [示例](#示例) +* [快速开始](#快速开始) +* [功能简介](#功能简介) +* [贡献代码](#贡献代码) +* [许可证](#许可证) + +## 概述 + +Qiniu-Flutter-SDK 基于七牛云存储官方 [API](https://developer.qiniu.com/kodo) 构建,提供抽象的接口用于快速使用七牛的对象存储功能。 + +Qiniu-Flutter-SDK 为客户端 SDK,没有包含 `token` 生成实现,为了安全,`token` 建议通过网络从服务端获取,具体生成代码可以参考官方[文档](https://developer.qiniu.com/kodo/manual/1208/upload-token),我们的很多服务端 SDK 已经实现了生成 Token 的功能,推荐直接使用,[查看其他 SDK](https://developer.qiniu.com/sdk#official-sdk) + +## 示例 + +请查看 [Example](https://github.com/qiniu/dart-sdk/tree/master/flutter/example) + +## 快速开始 + +编辑你的 `pubspec.yaml` 文件,在 `dependencies` 添加 `qiniu-flutter-sdk`,如下: + +```yaml +dependencies: + ... + qiniu-flutter-sdk: // 这里输入你需要的版本 +``` + +在你需要使用的地方 `import`,如下: + +```dart +import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart'; +``` + +### 快速使用 + +```dart + // 创建 storage 对象 + storage = Storage(); + // 使用 storage 的 putFile 对象进行文件上传 + storage.putFile(File('./file.txt'), 'TOKEN') + ..then(/* 上传成功 */) + ..catchError(/* 上传失败 */); +``` + +### 监听进度/状态 + +```dart + // 创建 storage 对象 + storage = Storage(); + + // 创建 Controller 对象 + putController = PutController(); + + // 添加整体进度监听 + putController.onProgress((double percent) { + print('任务进度变化:已发送:$percent'); + }); + + // 添加发送进度监听 + putController.onSendProgress((double percent) { + print('已上传进度变化:已发送:$percent'); + }); + + // 添加状态监听 + putController.addStatusListener((StorageStatus status) { + print('状态变化: 当前任务状态:$status'); + }); + + // 使用 storage 的 putFile 对象进行文件上传 + storage.putFile(File('./file.txt'), 'TOKEN', PutOptions( + controller: putController, + )) +``` + +### 取消正在上传的任务 + +```dart + // 创建 storage 对象 + storage = Storage(); + + // 创建 Controller 对象 + putController = PutController(); + + // 使用 storage 的 putFile 对象进行文件上传 + storage.putFile(File('./file.txt'), 'TOKEN', PutOptions( + controller: putController, + )) + + // 取消当前任务 + putController.cancel() +``` + +## API 说明 + +### `storage` + +使用前必须创建一个 `Storage` 实例 + +```dart + // 创建 storage 对象 + storage = Storage(); +``` + +同时,在构造 `Storage` 时可以传入一个 `Config` 控制内部的一些行为,如下: + +```dart + // 创建 storage 对象 + storage = Storage(Config( + // 通过自己的 hostProvider 来使用自己的 host 进行上传 + hostProvider: HostProvider, + // 可以通过实现 cacheProvider 来自己实现缓存系统支持分片断点续传 + cacheProvider: CacheProvider, + // 如果你需要对网络请求进行更基础的一些操作,你可以实现自己的 HttpClientAdapter 处理相关行为 + httpClientAdapter: HttpClientAdapter, + // 设定网络请求重试次数 + retryLimit: 3, + )); +``` + +#### `HostProvider` + +该接口是一个抽象的接口,大多数开发者不需要自己实现这个,除非你使用的是七牛的专/私有云服务,则可以通过实现自己的 `HostProvider` 来向自己的服务进行上传。 + +#### `CacheProvider` + +该接口同样是一个抽象的接口,`SDK` 支持分片断点续传功能,断点续传的信息通过 `CacheProvider` 提供的能力进行存储,如果你需要更好的体验,可以自己实现这个接口来对信息进行持久化的存储。 + +#### `HttpClientAdapter` + +该接口也是一个抽象的接口,如果你需要对网络请求进行进一步的自定义处理时,你可以通过实现一个 `HttpClientAdapter` 来接管 `SDK` 的所有请求。 + +#### `retryLimit` + + 用于限制内部重试逻辑的重试次数, 当发生一些可重试级别的错误时,`SDK` 会使用 `retryLimit` 的次数约束自动进行尝试。 + +#### `PutController` + +这里是一个重要的内容,对于整个上传任务的一些交互被封装到了这里, +`PutController` 用于对上传任务添加进度、状态的监听,同时可以通过 `PutController.cancel()` 对正在上传的任务进行取消。使用方式可以参考:[`取消正常上传的任务`](#取消正常上传的任务) + +#### `Storage.putFile` + +该接口内部封装了分片和直传两种实现,会根据文件的尺寸和上传配置信息自动选择使用分片还是直传的方式来上传对象 + +### 其他说明 + +1. 如果您想了解更多七牛的上传策略,建议您仔细阅读 [七牛官方文档-上传](https://developer.qiniu.com/kodo/manual/upload-types)。另外,七牛的上传策略是在后端服务指定的. + +## 功能列表 + +* 单文件上传 +* 分片上传 +* 任务状态 +* 任务进度 +* 上传进度 +* 失败重试 + +## 贡献代码 + +1. 登录 https://github.com + +2. Fork git@github.com:qiniu/dart-sdk.git + +3. 创建您的特性分支 (git checkout -b new-feature) + +4. 提交您的改动 (git commit -am 'Added some features or fixed a bug') + +5. 将您的改动记录提交到远程 git 仓库 (git push origin new-feature) + +6. 然后到 github 网站的该 git 远程仓库的 new-feature 分支下发起 Pull Request + +## 许可证 + +基于 Apache 2.0 协议发布 +> Copyright (c) 2020 qiniu.com diff --git a/qiniu-dart-sdk/flutter/analysis_options.yaml b/qiniu-dart-sdk/flutter/analysis_options.yaml new file mode 100644 index 00000000..96c1e8bf --- /dev/null +++ b/qiniu-dart-sdk/flutter/analysis_options.yaml @@ -0,0 +1,90 @@ +# copy from https://github.com/dart-lang/http/blob/master/analysis_options.yaml + +include: package:pedantic/analysis_options.yaml + +analyzer: + # enable-experiment: + # - non-nullable + + strong-mode: + implicit-casts: false + implicit-dynamic: false + +linter: + rules: + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_empty_else + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cascade_invocations + # comment_references + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - file_names + - hash_and_equals + - invariant_booleans + - iterable_contains_unrelated_type + - library_names + - library_prefixes + - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_conditional_assignment + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_collection_literals + - prefer_generic_function_type_aliases + - prefer_initializing_formals + - prefer_is_empty + - prefer_is_not_empty + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - valid_regexps + - void_checks diff --git a/qiniu-dart-sdk/flutter/lib/qiniu_flutter_sdk.dart b/qiniu-dart-sdk/flutter/lib/qiniu_flutter_sdk.dart new file mode 100644 index 00000000..5f14d9da --- /dev/null +++ b/qiniu-dart-sdk/flutter/lib/qiniu_flutter_sdk.dart @@ -0,0 +1,3 @@ +library qiniu_flutter_sdk; + +export './src/storage/storage.dart'; diff --git a/qiniu-dart-sdk/flutter/lib/src/storage/controller.dart b/qiniu-dart-sdk/flutter/lib/src/storage/controller.dart new file mode 100644 index 00000000..999c77b0 --- /dev/null +++ b/qiniu-dart-sdk/flutter/lib/src/storage/controller.dart @@ -0,0 +1,3 @@ +import 'package:qiniu_sdk_base/qiniu_sdk_base.dart' as base; + +class PutController extends base.PutController {} diff --git a/qiniu-dart-sdk/flutter/lib/src/storage/storage.dart b/qiniu-dart-sdk/flutter/lib/src/storage/storage.dart new file mode 100644 index 00000000..1e018293 --- /dev/null +++ b/qiniu-dart-sdk/flutter/lib/src/storage/storage.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:qiniu_sdk_base/qiniu_sdk_base.dart' as base; + +export 'package:qiniu_sdk_base/qiniu_sdk_base.dart' + show + Config, + PutOptions, + PutResponse, + HostProvider, + CacheProvider, + HttpClientAdapter, + QiniuError, + StorageError, + StorageErrorType, + PutByPartOptions, + StorageStatus, + PutBySingleOptions; + +export './controller.dart'; + +class Storage { + final base.Storage _baseStorage; + Storage({base.Config config}) : _baseStorage = base.Storage(config: config); + + Future putFile( + File file, + String token, { + base.PutOptions options, + }) { + return _baseStorage.putFile(file, token, options: options); + } +} diff --git a/qiniu-dart-sdk/flutter/pubspec.lock b/qiniu-dart-sdk/flutter/pubspec.lock new file mode 100644 index 00000000..e10c5c1c --- /dev/null +++ b/qiniu-dart-sdk/flutter/pubspec.lock @@ -0,0 +1,228 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "3fa83bc417e26f1d55814443a603747d48e3cdf55b2f4fea27dea8e9224dcefd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "3ce628f3c6a7144be6c524dbb20ca5af52fa59c850c587bf5e52cd70ece8e7e8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + dio: + dependency: transitive + description: + name: dio + sha256: "11979099d9ea182d74b6734340704d628b99c7a8316f9edd7718a297d1bcdd27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "9d2b0626e9e402fc98e6868360da8f256064d6c0b8e4c3edcca5e02fb0b95da9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.5" + matcher: + dependency: transitive + description: + name: matcher + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.13" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + pedantic: + dependency: "direct dev" + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + qiniu_sdk_base: + dependency: "direct main" + description: + path: "../base" + relative: true + source: path + version: "0.2.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=1.17.0" diff --git a/qiniu-dart-sdk/flutter/pubspec.yaml b/qiniu-dart-sdk/flutter/pubspec.yaml new file mode 100644 index 00000000..0ae3143e --- /dev/null +++ b/qiniu-dart-sdk/flutter/pubspec.yaml @@ -0,0 +1,23 @@ +name: qiniu_flutter_sdk +description: Qiniu Flutter sdk +version: 0.2.0 +author: qiniu +homepage: https://github.com/qiniu/dart-sdk/tree/master/flutter + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.17.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + qiniu_sdk_base: + path: ../base + +dev_dependencies: + pedantic: ^1.9.0 + flutter_test: + sdk: flutter + + +flutter: From 219bf7989e39b56e056b9246a73bff4e192883ea Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 17:05:42 +0800 Subject: [PATCH 69/81] convastion list message type show --- lib/im/chat_details_page.dart | 11 ++++++++++- lib/im/im_view/im_page.dart | 23 +++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index e99971cc..93d0a179 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -315,7 +315,15 @@ class _ChatDetailsPage extends State List filePath = mediaPaths.map((e) => e.path).toList(); Future.forEach(filePath.toSet(), (path) async { String fileUrl = await qiniu.uploadFile(apiService, path); - socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4); + socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4).then((value) { + Message message = value; + messages.insert(0, message); + chatController.clear(); + messageShowTime().then((value) { + refreshState(); + jumpToBottom(); + }); + }); }); } @@ -954,6 +962,7 @@ class _ChatDetailsPage extends State } Future fetchImageSize(String imagePath) async { + debugPrint("$imagePath"); Size size = Size.zero; Completer completer = Completer(); Image.file( diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 0fb970b7..d0d64d25 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -17,7 +17,6 @@ import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../retrofit/data/im_user.dart'; -import '../../retrofit/data/page.dart'; import '../../utils/flutter_utils.dart'; import '../../view_widget/custom_image.dart'; import 'on_chat_message.dart'; @@ -641,7 +640,7 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - lastMessageMap[conversationId]?.content ?? "", + messageContent(lastMessageMap[conversationId]), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -679,6 +678,26 @@ class _IMPage extends State implements OnChatMessage { ); } + String messageContent(Message message) { + if (message.msgType == 1) { + return message.content ?? ""; + } else if (message.msgType == 2) { + return "【图片】"; + } else if (message.msgType == 3) { + return "【语音】"; + } else if (message.msgType == 4) { + return "【视频】"; + } else if (message.msgType == 5) { + return "【红包】"; + } else if (message.msgType == 6) { + return "【转账】"; + } else if (message.msgType == 7) { + return "【位置】"; + } else { + return "【未知的消息类型】"; + } + } + Widget messageItem(img, title, messageNum) { return Container( padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 16.w, right: 15.w), From 2fa52a4ce2d17a78e88a02a11592598406cfcc2d Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Sun, 29 Sep 2024 17:08:17 +0800 Subject: [PATCH 70/81] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/im_view/im_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 0fb970b7..d9fe14e7 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -641,7 +641,7 @@ class _IMPage extends State implements OnChatMessage { children: [ Expanded( child: Text( - lastMessageMap[conversationId]?.content ?? "", + (lastMessageMap[conversationId]?.content ?? "").contains(".png")?"[图片]":lastMessageMap[conversationId]?.content, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( From 11f1ad42b022d98ad48e46e0166519a7d386cf9b Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Sun, 29 Sep 2024 17:10:25 +0800 Subject: [PATCH 71/81] 1 --- lib/im/im_view/im_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index d0d64d25..097aef2d 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -550,7 +550,6 @@ class _IMPage extends State implements OnChatMessage { ), ), onTap: () async { - // showDelDialog(conversationIds[position]); if (_isDelEnter) { await hxDatabase.deleteByUser(conversationIds[position]); lastScrollId = null; From 12277244a5bcc83cb5e7cbb68a8bc397bdddf906 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Sun, 29 Sep 2024 17:10:50 +0800 Subject: [PATCH 72/81] 1 --- lib/im/im_view/im_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 097aef2d..d0d64d25 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -550,6 +550,7 @@ class _IMPage extends State implements OnChatMessage { ), ), onTap: () async { + // showDelDialog(conversationIds[position]); if (_isDelEnter) { await hxDatabase.deleteByUser(conversationIds[position]); lastScrollId = null; From d09d390af6a079dfd153147a7f7c644fb4daab2b Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 17:38:25 +0800 Subject: [PATCH 73/81] convastion list message type show --- lib/im/chat_details_page.dart | 19 ++++++++++++------- lib/im/im_view/im_page.dart | 7 ++++++- lib/utils/qiniu.dart | 11 ++++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 93d0a179..6155f1e6 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -868,7 +868,7 @@ class _ChatDetailsPage extends State }, onTap:(){}, child: FutureBuilder( - future: fetchImageSize(messages[position].attach), + future: fetchImageSize(messages[position]), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { return Image.file( @@ -906,7 +906,7 @@ class _ChatDetailsPage extends State }, onTap:(){}, child: FutureBuilder( - future: fetchImageSize(messages[position].attach), + future: fetchImageSize(messages[position]), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { return Image.file( @@ -961,13 +961,18 @@ class _ChatDetailsPage extends State ); } - Future fetchImageSize(String imagePath) async { - debugPrint("$imagePath"); + Future fetchImageSize(Message message) async { + String imageLocalPath = message.attach; + debugPrint("$imageLocalPath"); + + File file = File(imageLocalPath); + if (!(await file.exists())) { + await qiniu.downloadFile(message.content, savePath: imageLocalPath); + } + Size size = Size.zero; Completer completer = Completer(); - Image.file( - File(imagePath), - ).image.resolve(ImageConfiguration()) + Image.file(file).image.resolve(ImageConfiguration()) .addListener(ImageStreamListener((image, synchronousCall) { size = Size((image.image.width ?? 0).toDouble(), (image.image.height ?? 0).toDouble()); size = resizeImage(size); diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index d0d64d25..ecece49a 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -700,7 +700,12 @@ class _IMPage extends State implements OnChatMessage { Widget messageItem(img, title, messageNum) { return Container( - padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 16.w, right: 15.w), + padding: EdgeInsets.only( + top: 8.h, + bottom: 8.h, + left: 16.w, + right: 15.w, + ), child: Column( children: [ Row( diff --git a/lib/utils/qiniu.dart b/lib/utils/qiniu.dart index bfcdb6a8..db0b5ba5 100644 --- a/lib/utils/qiniu.dart +++ b/lib/utils/qiniu.dart @@ -64,11 +64,16 @@ class Qiniu { } } - Dio dio = Dio(); + final Dio dio = Dio(); - Future downloadFile(String urlPath) async { + Future downloadFile(String urlPath, {String savePath}) async { Directory dir = await getTemporaryDirectory(); - File newFile = File("${dir.path}/hx_${urlPath.split('/').last}"); + File newFile; + if (savePath != null && savePath != '') { + newFile = File(savePath); + } else { + newFile = File("${dir.path}/hx_${urlPath.split('/').last}"); + } Response response = await dio.download(urlPath, newFile.path); if (response.statusCode == 200) { return newFile.path; From 45cfc3a22c63e1321201cd6cce23e6196ce51947 Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 17:47:49 +0800 Subject: [PATCH 74/81] convastion list message type show --- lib/im/SocketClient.dart | 1 + lib/im/chat_details_page.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index f35df628..ee04f98c 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -218,6 +218,7 @@ class SocketClient { final proto2 = Proto(5, 1, msgData.writeToBuffer()); try { _socket.add(proto2.toBytes()); + debugPrint("socket-send-success:"); } catch (e) { hxDatabase.update({"id": id, "state": 3}).catchError((error) { debugPrint("insertMessage: ${error.toString()}"); diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 6155f1e6..c39812ef 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -286,6 +286,7 @@ class _ChatDetailsPage extends State ///图片/视频选择 Future getImageOrVideo(GalleryMode galleryMode) async { if (selectCount == 0) return; + mediaPaths.clear(); List medias = await ImagePickers.pickerPaths( galleryMode: galleryMode, selectCount: (galleryMode == GalleryMode.video) ? 1 : selectCount, From f4e87c5b4b61300a12ebd5ebd79ae250e8e32d37 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Sun, 29 Sep 2024 17:47:59 +0800 Subject: [PATCH 75/81] =?UTF-8?q?=E5=A4=8D=E5=88=B6=E5=BC=B9=E6=A1=86?= =?UTF-8?q?=EF=BC=9B=20=E6=8B=8D=E7=85=A7=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 494 +++++++++++++++++++++++++--------- 1 file changed, 368 insertions(+), 126 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 93d0a179..10bba2c2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -17,6 +18,7 @@ import 'package:huixiang/utils/qiniu.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:image_pickers/image_pickers.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -24,9 +26,11 @@ import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../retrofit/data/base_data.dart'; +import '../retrofit/data/upload_result.dart'; import '../retrofit/data/user_info.dart'; import '../utils/flutter_utils.dart'; import '../view_widget/custom_image.dart'; +import '../view_widget/request_permission.dart'; import 'im_view/on_chat_message.dart'; import 'im_view/on_chat_msg_instance.dart'; @@ -330,6 +334,148 @@ class _ChatDetailsPage extends State setState(() {}); } + ///拍照 + openCamera() async { + if (await Permission.camera.isPermanentlyDenied) { + showCupertinoDialog( + context: context, + builder: (context) { + return RequestPermission( + "assets/image/icon_camera_permission_tips.webp", + S.of(context).ninxiangjiquanxianweikaiqi, + S.of(context).weilekaipaizhaoxuanzhetouxiang, + S.of(context).kaiqiquanxian, + (result) async { + if (result) { + await openAppSettings(); + } + }, + heightRatioWithWidth: 0.82, + ); + }); + }else if (await Permission.camera.isGranted) { + if (await Permission.storage.isPermanentlyDenied) { + showCupertinoDialog( + context: context, + builder: (context) { + return RequestPermission( + "assets/image/icon_storage_permission_tips.webp", + "您未开启存储权限,请点击开启", + "为了您可以在使用过程中访问您设备上的照片、媒体内容和文件,请您开启存储使用权限", + S.of(context).kaiqiquanxian, + (result) async { + if (result) { + await openAppSettings(); + } + }, + heightRatioWithWidth: 0.82, + ); + });}else if(await Permission.storage.isGranted){ + Media medias = await ImagePickers.openCamera( + cameraMimeType: CameraMimeType.photo, + cropConfig: CropConfig( + enableCrop: true, + width: 200, + height: 200, + ), + compressSize: 500, + ); + if (medias == null) return; + filePath = medias.path; + fileUpload(); + }else{ + showCameraTipsAlertDialog(context); + return false; + } + } else { + showCameraTipsAlertDialog(context); + return false; + } + } + String filePath; + + ///文件上传 + fileUpload() async { + if (filePath != null && filePath != "" && await File(filePath).exists()) { + BaseData baseData = await apiService + .upload(File(filePath), 123123123, false) + .catchError((onError) {}); + if (baseData != null && baseData.isSuccess) { + UploadResult uploadResult = baseData.data; + } + } + } + + ///拍照权限说明 + showCameraTipsAlertDialog(context) async { + //显示对话框 + showDialog( + context: context, + barrierDismissible: false, + barrierColor: null, + builder: (BuildContext context) { + return Column( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.all(15), + margin: EdgeInsets.all(15), + decoration: new BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding(padding:EdgeInsets.only(right: 10.w,), + child: Icon( + Icons.add_photo_alternate_outlined, + color: Colors.black, + size: 22, + )), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "相机权限使用说明", + style: TextStyle( + fontSize: 18.sp, + fontWeight: MyFontWeight.regular, + color: Colors.black, + ), + ), + SizedBox( + height: 3.h, + ), + Text( + "实现您扫码、拍摄等功能。", + style: TextStyle( + fontSize: 13.sp, + height: 1.2, + fontWeight: MyFontWeight.regular, + color: Colors.black, + ), + ), + ], + ), + ), + ], + ), + ) + ], + mainAxisSize: MainAxisSize.min, + ); + }, + ); + await Permission.camera.request(); + Navigator.of(context).pop(); + if(await Permission.camera.isGranted){ + openCamera(); + } + } + @override Widget build(BuildContext context) { double h = MediaQuery.of(context).viewInsets.bottom; @@ -449,6 +595,7 @@ class _ChatDetailsPage extends State bool isSelf = messages[position].fromId == selfUserId; bool isText = messages[position].msgType == 1; bool isImage = messages[position].msgType == 2; + GlobalKey _buttonKey = GlobalKey(); return Container( padding: EdgeInsets.only( top: 16.h, @@ -616,30 +763,120 @@ class _ChatDetailsPage extends State Expanded( child: Container( alignment: Alignment.centerLeft, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), - padding: EdgeInsets.symmetric( - vertical: 8.h, - horizontal: 12.w, - ), - child: GestureDetector( - onLongPress: () { - setState(() { - copyIndex = 1; - }); - }, - child: Text( + child:GestureDetector( + onLongPress: () { + final RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; + final buttonPosition = renderBox.localToGlobal(Offset.zero); + final buttonSize = renderBox.size; + + setState(() { + // showMenu( + // context: context, + // color: Color(0xFF2A2A2A), + // position: RelativeRect.fromLTRB( + // buttonPosition.dx + (buttonSize.width - 152.w) / 2, // 居中对齐 + // buttonPosition.dy - 55, + // buttonPosition.dx + buttonSize.width, + // buttonPosition.dy + // ), /// 设置弹出菜单的显示位置 + // items: [ + // PopupMenuItem( + // value: 1, + // height: 30.h, + // padding: EdgeInsets.zero, + // child:Container( + // child:Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // GestureDetector( + // behavior: HitTestBehavior.translucent, + // onTap: () { + // setState(() { + // copyIndex = 0; + // this.copy(messages[position].content); + // Navigator.pop(context); + // }); + // }, + // child:Padding( + // padding:EdgeInsets.only(left: 28.w,right:25.w), + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_copy.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // "复制", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ), + // ), + // GestureDetector( + // behavior: HitTestBehavior.translucent, + // onTap: () {}, + // child: Padding( + // padding:EdgeInsets.only(right: 28.w,left:25.w), + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_delete.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // S.of(context).shanchu, + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ) + // ], + // ), + // ), + // ), + // ], + // ); + }); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + key: _buttonKey, + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + child: Text( tex = messages[position].content, textAlign: TextAlign.left, style: TextStyle( @@ -778,7 +1015,102 @@ class _ChatDetailsPage extends State Expanded( child: Container( alignment: Alignment.centerRight, - child: Container( + child: GestureDetector( + onLongPress: () { + final RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; + final buttonPosition = renderBox.localToGlobal(Offset.zero); + final buttonSize = renderBox.size; + + setState(() { + // showMenu( + // context: context, + // color: Color(0xFF2A2A2A), + // position: RelativeRect.fromLTRB( + // buttonPosition.dx + (buttonSize.width - 152.w) / 2, // 居中对齐 + // buttonPosition.dy - 55, + // buttonPosition.dx + buttonSize.width, + // buttonPosition.dy + // ), /// 设置弹出菜单的显示位置 + // items: [ + // PopupMenuItem( + // value: 1, + // height: 30.h, + // padding: EdgeInsets.zero, + // child:Container( + // child:Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // GestureDetector( + // behavior: HitTestBehavior.translucent, + // onTap: () { + // setState(() { + // copyIndex = 0; + // this.copy(messages[position].content); + // Navigator.pop(context); + // }); + // }, + // child:Padding( + // padding:EdgeInsets.only(left: 28.w,right:25.w), + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_copy.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // "复制", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // ), + // ), + // ), + // GestureDetector( + // behavior: HitTestBehavior.translucent, + // onTap: () {}, + // child: Padding( + // padding:EdgeInsets.only(right: 28.w,left:25.w), + // child: Column( + // children: [ + // Image.asset( + // "assets/image/icon_chat_delete.webp", + // height: 16, + // width: 16, + // ), + // SizedBox( + // height: 2.h, + // ), + // Text( + // S.of(context).shanchu, + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ], + // )), + // ) + // ], + // ), + // ), + // ), + // ], + // ); + }); + }, + child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Color(0xFF32A060), @@ -791,30 +1123,23 @@ class _ChatDetailsPage extends State ), ], ), + key: _buttonKey, padding: EdgeInsets.symmetric( vertical: 8.h, horizontal: 12.w, ), - child: GestureDetector( - onLongPress: () { - setState(() { - copyIndex = 1; - // showCustomDialog(position); - }); - }, - child: Text( - tex = messages[position].content, - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Colors.white, - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, - ), + child: Text( + tex = messages[position].content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Colors.white, + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, ), ), ), - ), + )), ), SizedBox( width: 12.w, @@ -1146,7 +1471,7 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - getImageOrVideo(GalleryMode.video); + openCamera(); }, child: Column( children: [ @@ -1237,89 +1562,6 @@ class _ChatDetailsPage extends State ); } - ///复制弹窗 - showCustomDialog(int position) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.transparent, - elevation: 0, - content: Container( - // width: 60.w, - height:50.h, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric( - horizontal: 32.w, - vertical: 7.5.h, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - copyIndex = 0; - this.copy(tex); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () {}, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ], - ), - ) - ], - ), - ) - ); - }); - } - /// 复制文本 copy(String tex) { print(tex); From d3d7bff8b6c8521c761cb4ae0258ad07b0f7ed6d Mon Sep 17 00:00:00 2001 From: zsw Date: Sun, 29 Sep 2024 18:31:11 +0800 Subject: [PATCH 76/81] convastion list message type show --- lib/im/chat_details_page.dart | 63 ++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index a8b55de0..a1471727 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -135,15 +135,17 @@ class _ChatDetailsPage extends State } } + SharedPreferences sharedPre; + ///查询个人信息 queryUser() async { - final SharedPreferences value = await SharedPreferences.getInstance(); + sharedPre = await SharedPreferences.getInstance(); if (apiService == null) - apiService = ApiService(Dio(), context: context, token: value.getString("token")); - if (value.containsKey('user') && - value.getString('user') != null && - value.getString('user') != "") { - userInfo = UserInfo.fromJson(jsonDecode(value.getString('user'))); + apiService = ApiService(Dio(), context: context, token: sharedPre.getString("token")); + if (sharedPre.containsKey('user') && + sharedPre.getString('user') != null && + sharedPre.getString('user') != "") { + userInfo = UserInfo.fromJson(jsonDecode(sharedPre.getString('user'))); if (userInfo.phone?.isNotEmpty ?? false) { setState(() {}); return; @@ -382,8 +384,17 @@ class _ChatDetailsPage extends State compressSize: 500, ); if (medias == null) return; - filePath = medias.path; - fileUpload(); + String path = medias.path; + String fileUrl = await qiniu.uploadFile(apiService, path); + socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: 2).then((value) { + Message message = value; + messages.insert(0, message); + chatController.clear(); + messageShowTime().then((value) { + refreshState(); + jumpToBottom(); + }); + }); }else{ showCameraTipsAlertDialog(context); return false; @@ -393,19 +404,6 @@ class _ChatDetailsPage extends State return false; } } - String filePath; - - ///文件上传 - fileUpload() async { - if (filePath != null && filePath != "" && await File(filePath).exists()) { - BaseData baseData = await apiService - .upload(File(filePath), 123123123, false) - .catchError((onError) {}); - if (baseData != null && baseData.isSuccess) { - UploadResult uploadResult = baseData.data; - } - } - } ///拍照权限说明 showCameraTipsAlertDialog(context) async { @@ -1297,11 +1295,28 @@ class _ChatDetailsPage extends State } Size size = Size.zero; + + if (sharedPre != null) { + String sizeStr = sharedPre.getString(imageLocalPath); + if (sizeStr != null && sizeStr != "") { + Map sizeMap = jsonDecode(sizeStr); + size = Size(sizeMap["width"], sizeMap["height"]); + if (size != Size.zero) { + return size; + } + } + } + Completer completer = Completer(); Image.file(file).image.resolve(ImageConfiguration()) .addListener(ImageStreamListener((image, synchronousCall) { size = Size((image.image.width ?? 0).toDouble(), (image.image.height ?? 0).toDouble()); size = resizeImage(size); + Map sizeMap = { + "width": size.width, + "height": size.height, + }; + sharedPre.setString(imageLocalPath, jsonEncode(sizeMap)); completer.complete(size); })); return completer.future; @@ -1399,9 +1414,9 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - _onMoreTap(); - jumpToBottom(); - // SmartDialog.showToast("暂不支持", alignment: Alignment.center); + // _onMoreTap(); + // jumpToBottom(); + SmartDialog.showToast("暂不支持", alignment: Alignment.center); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), From 7d003d968fd6af7656a1c096f471035200b5d80f Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Mon, 30 Sep 2024 13:57:53 +0800 Subject: [PATCH 77/81] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=88=A0=E9=99=A4=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 568 ++++++++++++------------------- lib/im/database/hx_database.dart | 4 + 2 files changed, 218 insertions(+), 354 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index a1471727..1a141630 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -14,7 +14,6 @@ import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; -import 'package:huixiang/utils/qiniu.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:image_pickers/image_pickers.dart'; @@ -26,7 +25,6 @@ import '../../community/release_dynamic.dart'; import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../retrofit/data/base_data.dart'; -import '../retrofit/data/upload_result.dart'; import '../retrofit/data/user_info.dart'; import '../utils/flutter_utils.dart'; import '../view_widget/custom_image.dart'; @@ -68,7 +66,6 @@ class _ChatDetailsPage extends State OnChatMsgInstance.instance.onChatMessage; final ScrollController scrollController = ScrollController(); String tex = ""; - int copyIndex = 0; String selfUserId = ""; ImUser _toUser; String conversation; @@ -574,7 +571,6 @@ class _ChatDetailsPage extends State behavior: HitTestBehavior.opaque, onTap: () { setState(() { - copyIndex = 0; if (emojiShowing) { emojiShowing = !emojiShowing; } @@ -638,90 +634,7 @@ class _ChatDetailsPage extends State // SizedBox( // height: 16.h, // ), - // if (copyIndex == -1) - // Stack( - // alignment: Alignment.bottomCenter, - // children: [ - // Container( - // padding: EdgeInsets.only(bottom: 13.h), - // child: Container( - // width: 180.w, - // decoration: BoxDecoration( - // color: Color(0xFF2A2A2A), - // borderRadius: BorderRadius.circular(6), - // ), - // padding: EdgeInsets.symmetric( - // horizontal: 32.w, - // vertical: 7.5.h, - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // GestureDetector( - // onTap: () { - // setState(() { - // copyIndex = 0; - // this.copy(tex); - // }); - // }, - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_copy.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // "复制", - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ), - // GestureDetector( - // onTap: () {}, - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_delete.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // S.of(context).shanchu, - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ) - // ], - // ), - // ), - // ), - // Image.asset( - // "assets/image/icon_copy_j.webp", - // height: 17, - // width: 17, - // ), - // ], - // ), + /// not self if (!isSelf && isText) @@ -764,98 +677,113 @@ class _ChatDetailsPage extends State alignment: Alignment.centerLeft, child:GestureDetector( onLongPress: () { - final RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; - final buttonPosition = renderBox.localToGlobal(Offset.zero); - final buttonSize = renderBox.size; + RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; + Offset buttonPosition = renderBox.localToGlobal(Offset.zero); + Size buttonSize = renderBox.size; - setState(() { - // showMenu( - // context: context, - // color: Color(0xFF2A2A2A), - // position: RelativeRect.fromLTRB( - // buttonPosition.dx + (buttonSize.width - 152.w) / 2, // 居中对齐 - // buttonPosition.dy - 55, - // buttonPosition.dx + buttonSize.width, - // buttonPosition.dy - // ), /// 设置弹出菜单的显示位置 - // items: [ - // PopupMenuItem( - // value: 1, - // height: 30.h, - // padding: EdgeInsets.zero, - // child:Container( - // child:Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // GestureDetector( - // behavior: HitTestBehavior.translucent, - // onTap: () { - // setState(() { - // copyIndex = 0; - // this.copy(messages[position].content); - // Navigator.pop(context); - // }); - // }, - // child:Padding( - // padding:EdgeInsets.only(left: 28.w,right:25.w), - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_copy.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // "复制", - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ), - // ), - // GestureDetector( - // behavior: HitTestBehavior.translucent, - // onTap: () {}, - // child: Padding( - // padding:EdgeInsets.only(right: 28.w,left:25.w), - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_delete.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // S.of(context).shanchu, - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // )), - // ) - // ], - // ), - // ), - // ), - // ], - // ); - }); + showMenu( + context: context, + //去除阴影 + elevation: 0, + color: Colors.transparent, + position: RelativeRect.fromLTRB( + buttonPosition.dx + (buttonSize.width - 180.w) / 2, // 居中对齐 + buttonPosition.dy - 68, + buttonPosition.dx + buttonSize.width, + buttonPosition.dy + ), /// 设置弹出菜单的显示位置 + items: [ + PopupMenuItem( + value: 1, + padding: EdgeInsets.zero, + child:Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.only(bottom: 13.h), + child: Container( + width: 180.w, + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(6), + ), + padding: + EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() { + this.copy(messages[position].content); + Navigator.pop(context); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + "复制", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () async{ + await hxDatabase.deleteByMsgId(messages[position].id.toString()); + messages.removeAt(position); + Navigator.pop(context); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ) + ], + ), + ), + ), + Image.asset( + "assets/image/icon_copy_j.webp", + height: 17, + width: 17, + ), + ], + ), + ), + ], + ); }, child: Container( decoration: BoxDecoration( @@ -893,89 +821,6 @@ class _ChatDetailsPage extends State ), ), - // if (copyIndex == 1) - // Stack( - // alignment: Alignment.bottomCenter, - // children: [ - // Container( - // padding: EdgeInsets.only(bottom: 13.h), - // child: Container( - // width: 180.w, - // decoration: BoxDecoration( - // color: Color(0xFF2A2A2A), - // borderRadius: BorderRadius.circular(6), - // ), - // padding: - // EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // GestureDetector( - // onTap: () { - // setState(() { - // copyIndex = 0; - // this.copy(tex); - // }); - // }, - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_copy.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // "复制", - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ), - // GestureDetector( - // onTap: () {}, - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_delete.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // S.of(context).shanchu, - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ) - // ], - // ), - // ), - // ), - // Image.asset( - // "assets/image/icon_copy_j.webp", - // height: 17, - // width: 17, - // ), - // ], - // ), - /// self if (isSelf && isText) SizedBox( @@ -1014,100 +859,115 @@ class _ChatDetailsPage extends State Expanded( child: Container( alignment: Alignment.centerRight, - child: GestureDetector( + child: InkWell( onLongPress: () { - final RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; - final buttonPosition = renderBox.localToGlobal(Offset.zero); - final buttonSize = renderBox.size; + RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; + Offset buttonPosition = renderBox.localToGlobal(Offset.zero); + Size buttonSize = renderBox.size; - setState(() { - // showMenu( - // context: context, - // color: Color(0xFF2A2A2A), - // position: RelativeRect.fromLTRB( - // buttonPosition.dx + (buttonSize.width - 152.w) / 2, // 居中对齐 - // buttonPosition.dy - 55, - // buttonPosition.dx + buttonSize.width, - // buttonPosition.dy - // ), /// 设置弹出菜单的显示位置 - // items: [ - // PopupMenuItem( - // value: 1, - // height: 30.h, - // padding: EdgeInsets.zero, - // child:Container( - // child:Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // GestureDetector( - // behavior: HitTestBehavior.translucent, - // onTap: () { - // setState(() { - // copyIndex = 0; - // this.copy(messages[position].content); - // Navigator.pop(context); - // }); - // }, - // child:Padding( - // padding:EdgeInsets.only(left: 28.w,right:25.w), - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_copy.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // "复制", - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // ), - // ), - // ), - // GestureDetector( - // behavior: HitTestBehavior.translucent, - // onTap: () {}, - // child: Padding( - // padding:EdgeInsets.only(right: 28.w,left:25.w), - // child: Column( - // children: [ - // Image.asset( - // "assets/image/icon_chat_delete.webp", - // height: 16, - // width: 16, - // ), - // SizedBox( - // height: 2.h, - // ), - // Text( - // S.of(context).shanchu, - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // fontWeight: MyFontWeight.regular, - // ), - // ), - // ], - // )), - // ) - // ], - // ), - // ), - // ), - // ], - // ); - }); + showMenu( + context: context, + //去除阴影 + elevation: 0, + color: Colors.transparent, + position: RelativeRect.fromLTRB( + buttonPosition.dx + (buttonSize.width - 180.w) / 2, // 居中对齐 + buttonPosition.dy - 68, + buttonPosition.dx + buttonSize.width, + buttonPosition.dy + ), /// 设置弹出菜单的显示位置 + items: [ + PopupMenuItem( + value: 1, + padding: EdgeInsets.zero, + child:Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.only(bottom: 13.h), + child: Container( + width: 180.w, + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(6), + ), + padding: + EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() { + this.copy(messages[position].content); + Navigator.pop(context); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + "复制", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () async{ + await hxDatabase.deleteByMsgId(messages[position].id.toString()); + messages.removeAt(position); + Navigator.pop(context); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ) + ], + ), + ), + ), + Image.asset( + "assets/image/icon_copy_j.webp", + height: 17, + width: 17, + ), + ], + ), + ), + ], + ); }, child: Container( decoration: BoxDecoration( diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index 4daa8573..94566b14 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -131,6 +131,10 @@ class HxDatabase { return db.delete("Message",where: "conversationId = ?", whereArgs: [conversationId]); } + Future deleteByMsgId(String id) async { + return db.delete("Message",where: "id = ?", whereArgs: [id]); + } + Future deleteAll() async { return db.delete("Message"); } From 92064b15b77b37729c8e29db32422d2e644ad104 Mon Sep 17 00:00:00 2001 From: wurong <953969641@qq.com> Date: Mon, 30 Sep 2024 17:15:46 +0800 Subject: [PATCH 78/81] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=88=A0=E9=99=A4=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A4=84=E7=90=86=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/chat_details_page.dart | 452 +++++++++++++++++++--------------- lib/im/chat_friend_group.dart | 4 - lib/im/chat_setting.dart | 3 - lib/im/contact_share.dart | 2 - lib/im/im_search.dart | 1 - 5 files changed, 248 insertions(+), 214 deletions(-) diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 1a141630..6f47ade2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -138,7 +138,8 @@ class _ChatDetailsPage extends State queryUser() async { sharedPre = await SharedPreferences.getInstance(); if (apiService == null) - apiService = ApiService(Dio(), context: context, token: sharedPre.getString("token")); + apiService = ApiService(Dio(), + context: context, token: sharedPre.getString("token")); if (sharedPre.containsKey('user') && sharedPre.getString('user') != null && sharedPre.getString('user') != "") { @@ -179,8 +180,7 @@ class _ChatDetailsPage extends State void jumpToBottom() { Future.delayed(const Duration(milliseconds: 100), () { - scrollController.position - .jumpTo(0); + scrollController.position.jumpTo(0); }); } @@ -319,7 +319,10 @@ class _ChatDetailsPage extends State List filePath = mediaPaths.map((e) => e.path).toList(); Future.forEach(filePath.toSet(), (path) async { String fileUrl = await qiniu.uploadFile(apiService, path); - socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4).then((value) { + socketClient + .sendMessage(_toUser.mid, fileUrl, + attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4) + .then((value) { Message message = value; messages.insert(0, message); chatController.clear(); @@ -329,7 +332,6 @@ class _ChatDetailsPage extends State }); }); }); - } setState(() {}); } @@ -345,7 +347,7 @@ class _ChatDetailsPage extends State S.of(context).ninxiangjiquanxianweikaiqi, S.of(context).weilekaipaizhaoxuanzhetouxiang, S.of(context).kaiqiquanxian, - (result) async { + (result) async { if (result) { await openAppSettings(); } @@ -353,7 +355,7 @@ class _ChatDetailsPage extends State heightRatioWithWidth: 0.82, ); }); - }else if (await Permission.camera.isGranted) { + } else if (await Permission.camera.isGranted) { if (await Permission.storage.isPermanentlyDenied) { showCupertinoDialog( context: context, @@ -363,14 +365,15 @@ class _ChatDetailsPage extends State "您未开启存储权限,请点击开启", "为了您可以在使用过程中访问您设备上的照片、媒体内容和文件,请您开启存储使用权限", S.of(context).kaiqiquanxian, - (result) async { + (result) async { if (result) { await openAppSettings(); } }, heightRatioWithWidth: 0.82, ); - });}else if(await Permission.storage.isGranted){ + }); + } else if (await Permission.storage.isGranted) { Media medias = await ImagePickers.openCamera( cameraMimeType: CameraMimeType.photo, cropConfig: CropConfig( @@ -383,7 +386,9 @@ class _ChatDetailsPage extends State if (medias == null) return; String path = medias.path; String fileUrl = await qiniu.uploadFile(apiService, path); - socketClient.sendMessage(_toUser.mid, fileUrl, attach: path, msgType: 2).then((value) { + socketClient + .sendMessage(_toUser.mid, fileUrl, attach: path, msgType: 2) + .then((value) { Message message = value; messages.insert(0, message); chatController.clear(); @@ -392,7 +397,7 @@ class _ChatDetailsPage extends State jumpToBottom(); }); }); - }else{ + } else { showCameraTipsAlertDialog(context); return false; } @@ -423,7 +428,10 @@ class _ChatDetailsPage extends State child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding:EdgeInsets.only(right: 10.w,), + Padding( + padding: EdgeInsets.only( + right: 10.w, + ), child: Icon( Icons.add_photo_alternate_outlined, color: Colors.black, @@ -467,7 +475,7 @@ class _ChatDetailsPage extends State ); await Permission.camera.request(); Navigator.of(context).pop(); - if(await Permission.camera.isGranted){ + if (await Permission.camera.isGranted) { openCamera(); } } @@ -635,7 +643,6 @@ class _ChatDetailsPage extends State // height: 16.h, // ), - /// not self if (!isSelf && isText) SizedBox( @@ -649,25 +656,25 @@ class _ChatDetailsPage extends State mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).pushNamed( - '/router/personal_page', - arguments: { - "memberId": _toUser?.mid ?? "", - "inletType": 1 - }, - ); - }, - child: MImage( - _toUser.avatar, - isCircle: true, - height: 44.h, - width: 44.h, - fit: BoxFit.cover, - errorSrc: "assets/image/default_1.webp", - fadeSrc: "assets/image/default_1.webp", - ), + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/personal_page', + arguments: { + "memberId": _toUser?.mid ?? "", + "inletType": 1 + }, + ); + }, + child: MImage( + _toUser.avatar, + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + ), ), SizedBox( width: 12.w, @@ -675,10 +682,12 @@ class _ChatDetailsPage extends State Expanded( child: Container( alignment: Alignment.centerLeft, - child:GestureDetector( + child: GestureDetector( onLongPress: () { - RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; - Offset buttonPosition = renderBox.localToGlobal(Offset.zero); + RenderBox renderBox = _buttonKey.currentContext + .findRenderObject() as RenderBox; + Offset buttonPosition = + renderBox.localToGlobal(Offset.zero); Size buttonSize = renderBox.size; showMenu( @@ -686,17 +695,19 @@ class _ChatDetailsPage extends State //去除阴影 elevation: 0, color: Colors.transparent, - position: RelativeRect.fromLTRB( - buttonPosition.dx + (buttonSize.width - 180.w) / 2, // 居中对齐 + position: RelativeRect.fromLTRB( + buttonPosition.dx + + (buttonSize.width - 180.w) / 2, // 居中对齐 buttonPosition.dy - 68, buttonPosition.dx + buttonSize.width, - buttonPosition.dy - ), /// 设置弹出菜单的显示位置 + buttonPosition.dy), + + /// 设置弹出菜单的显示位置 items: [ PopupMenuItem( value: 1, padding: EdgeInsets.zero, - child:Stack( + child: Stack( alignment: Alignment.bottomCenter, children: [ Container( @@ -705,18 +716,22 @@ class _ChatDetailsPage extends State width: 180.w, decoration: BoxDecoration( color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), + borderRadius: + BorderRadius.circular(6), ), - padding: - EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + padding: EdgeInsets.symmetric( + horizontal: 32.w, vertical: 7.5.h), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.center, children: [ GestureDetector( onTap: () { setState(() { - this.copy(messages[position].content); + this.copy(messages[position] + .content); Navigator.pop(context); }); }, @@ -736,15 +751,19 @@ class _ChatDetailsPage extends State style: TextStyle( color: Colors.white, fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + fontWeight: + MyFontWeight.regular, ), ), ], ), ), GestureDetector( - onTap: () async{ - await hxDatabase.deleteByMsgId(messages[position].id.toString()); + onTap: () async { + await hxDatabase.deleteByMsgId( + messages[position] + .id + .toString()); messages.removeAt(position); Navigator.pop(context); }, @@ -764,7 +783,8 @@ class _ChatDetailsPage extends State style: TextStyle( color: Colors.white, fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + fontWeight: + MyFontWeight.regular, ), ), ], @@ -803,7 +823,7 @@ class _ChatDetailsPage extends State vertical: 8.h, horizontal: 12.w, ), - child: Text( + child: Text( tex = messages[position].content, textAlign: TextAlign.left, style: TextStyle( @@ -858,11 +878,13 @@ class _ChatDetailsPage extends State ), Expanded( child: Container( - alignment: Alignment.centerRight, - child: InkWell( - onLongPress: () { - RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; - Offset buttonPosition = renderBox.localToGlobal(Offset.zero); + alignment: Alignment.centerRight, + child: InkWell( + onLongPress: () { + RenderBox renderBox = _buttonKey.currentContext + .findRenderObject() as RenderBox; + Offset buttonPosition = + renderBox.localToGlobal(Offset.zero); Size buttonSize = renderBox.size; showMenu( @@ -870,17 +892,19 @@ class _ChatDetailsPage extends State //去除阴影 elevation: 0, color: Colors.transparent, - position: RelativeRect.fromLTRB( - buttonPosition.dx + (buttonSize.width - 180.w) / 2, // 居中对齐 + position: RelativeRect.fromLTRB( + buttonPosition.dx + + (buttonSize.width - 180.w) / 2, // 居中对齐 buttonPosition.dy - 68, buttonPosition.dx + buttonSize.width, - buttonPosition.dy - ), /// 设置弹出菜单的显示位置 + buttonPosition.dy), + + /// 设置弹出菜单的显示位置 items: [ PopupMenuItem( value: 1, padding: EdgeInsets.zero, - child:Stack( + child: Stack( alignment: Alignment.bottomCenter, children: [ Container( @@ -889,18 +913,23 @@ class _ChatDetailsPage extends State width: 180.w, decoration: BoxDecoration( color: Color(0xFF2A2A2A), - borderRadius: BorderRadius.circular(6), + borderRadius: + BorderRadius.circular(6), ), - padding: - EdgeInsets.symmetric(horizontal: 32.w, vertical: 7.5.h), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 7.5.h), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.center, children: [ GestureDetector( onTap: () { setState(() { - this.copy(messages[position].content); + this.copy(messages[position] + .content); Navigator.pop(context); }); }, @@ -916,19 +945,25 @@ class _ChatDetailsPage extends State ), Text( "复制", - textAlign: TextAlign.center, + textAlign: + TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + fontWeight: MyFontWeight + .regular, ), ), ], ), ), GestureDetector( - onTap: () async{ - await hxDatabase.deleteByMsgId(messages[position].id.toString()); + onTap: () async { + await hxDatabase + .deleteByMsgId( + messages[position] + .id + .toString()); messages.removeAt(position); Navigator.pop(context); }, @@ -944,11 +979,13 @@ class _ChatDetailsPage extends State ), Text( S.of(context).shanchu, - textAlign: TextAlign.center, + textAlign: + TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 12.sp, - fontWeight: MyFontWeight.regular, + fontWeight: MyFontWeight + .regular, ), ), ], @@ -968,61 +1005,61 @@ class _ChatDetailsPage extends State ), ], ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFF32A060), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFF32A060), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + key: _buttonKey, + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + child: Text( + tex = messages[position].content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Colors.white, + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), ), - ], - ), - key: _buttonKey, - padding: EdgeInsets.symmetric( - vertical: 8.h, - horizontal: 12.w, - ), - child: Text( - tex = messages[position].content, - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Colors.white, - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, ), - ), - ), - )), + )), ), SizedBox( width: 12.w, ), GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).pushNamed( - '/router/personal_page', - arguments: { - "memberId": "0", - "inletType": 1, - }, - ); - }, - child: MImage( - userInfo?.headimg ?? "", - isCircle: true, - height: 44.h, - width: 44.h, - fit: BoxFit.cover, - errorSrc: "assets/image/default_1.webp", - fadeSrc: "assets/image/default_1.webp", - ), + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pushNamed( + '/router/personal_page', + arguments: { + "memberId": "0", + "inletType": 1, + }, + ); + }, + child: MImage( + userInfo?.headimg ?? "", + isCircle: true, + height: 44.h, + width: 44.h, + fit: BoxFit.cover, + errorSrc: "assets/image/default_1.webp", + fadeSrc: "assets/image/default_1.webp", + ), ), ], ), @@ -1050,11 +1087,13 @@ class _ChatDetailsPage extends State onLongPress: () { setState(() {}); }, - onTap:(){}, + onTap: () {}, child: FutureBuilder( future: fetchImageSize(messages[position]), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { + if (snapshot.connectionState == + ConnectionState.waiting || + snapshot.hasError) { return Image.file( File(messages[position].attach), width: 180.w, @@ -1088,11 +1127,13 @@ class _ChatDetailsPage extends State onLongPress: () { setState(() {}); }, - onTap:(){}, + onTap: () {}, child: FutureBuilder( future: fetchImageSize(messages[position]), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { + if (snapshot.connectionState == + ConnectionState.waiting || + snapshot.hasError) { return Image.file( File(messages[position].attach), width: 180.w, @@ -1168,9 +1209,12 @@ class _ChatDetailsPage extends State } Completer completer = Completer(); - Image.file(file).image.resolve(ImageConfiguration()) + Image.file(file) + .image + .resolve(ImageConfiguration()) .addListener(ImageStreamListener((image, synchronousCall) { - size = Size((image.image.width ?? 0).toDouble(), (image.image.height ?? 0).toDouble()); + size = Size((image.image.width ?? 0).toDouble(), + (image.image.height ?? 0).toDouble()); size = resizeImage(size); Map sizeMap = { "width": size.width, @@ -1229,7 +1273,8 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient.sendMessage(_toUser.mid, commentText) + socketClient + .sendMessage(_toUser.mid, commentText) .then((value) { Message message = value; messages.insert(0, message); @@ -1263,12 +1308,12 @@ class _ChatDetailsPage extends State jumpToBottom(); }, child: Container( - padding: EdgeInsets.only(left: 15.w, right: 8.w), - child: Image.asset( - "assets/image/icon_chat_emo.webp", - height: 26.h, - width: 26.h, - ), + padding: EdgeInsets.only(left: 15.w, right: 8.w), + child: Image.asset( + "assets/image/icon_chat_emo.webp", + height: 26.h, + width: 26.h, + ), ), ), GestureDetector( @@ -1297,40 +1342,40 @@ class _ChatDetailsPage extends State Offstage( offstage: !emojiShowing, child: SizedBox( - height: keyboard == -1 ? 270 : keyboard, - width: MediaQuery.of(context).size.width, - child: EmojiPicker( - textEditingController: chatController, - config: Config( - columns: 7, - emojiSizeMax: 32 * (Platform.isIOS ? 1.10 : 1.0), - verticalSpacing: 0, - horizontalSpacing: 0, - gridPadding: EdgeInsets.zero, - initCategory: Category.RECENT, - bgColor: const Color(0xFFF2F2F2), - // indicatorColor: Colors.blue, - iconColor: Colors.grey, - iconColorSelected: Colors.blue, - backspaceColor: Colors.blue, - skinToneDialogBgColor: Colors.white, - skinToneIndicatorColor: Colors.grey, - enableSkinTones: true, - showRecentsTab: true, - recentsLimit: 28, - replaceEmojiOnLimitExceed: false, - noRecents: Text( - "最近使用", - style: TextStyle(fontSize: 20, color: Colors.black26), - textAlign: TextAlign.center, - ), - loadingIndicator: const SizedBox.shrink(), - tabIndicatorAnimDuration: Duration(milliseconds: 0), - categoryIcons: const CategoryIcons(), - buttonMode: ButtonMode.MATERIAL, - checkPlatformCompatibility: true, + height: keyboard == -1 ? 270 : keyboard, + width: MediaQuery.of(context).size.width, + child: EmojiPicker( + textEditingController: chatController, + config: Config( + columns: 7, + emojiSizeMax: 32 * (Platform.isIOS ? 1.10 : 1.0), + verticalSpacing: 0, + horizontalSpacing: 0, + gridPadding: EdgeInsets.zero, + initCategory: Category.RECENT, + bgColor: const Color(0xFFF2F2F2), + // indicatorColor: Colors.blue, + iconColor: Colors.grey, + iconColorSelected: Colors.blue, + backspaceColor: Colors.blue, + skinToneDialogBgColor: Colors.white, + skinToneIndicatorColor: Colors.grey, + enableSkinTones: true, + showRecentsTab: true, + recentsLimit: 28, + replaceEmojiOnLimitExceed: false, + noRecents: Text( + "最近使用", + style: TextStyle(fontSize: 20, color: Colors.black26), + textAlign: TextAlign.center, ), + loadingIndicator: const SizedBox.shrink(), + tabIndicatorAnimDuration: Duration(milliseconds: 0), + categoryIcons: const CategoryIcons(), + buttonMode: ButtonMode.MATERIAL, + checkPlatformCompatibility: true, ), + ), ), ), @@ -1392,45 +1437,45 @@ class _ChatDetailsPage extends State ), ), GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - getImageOrVideo(GalleryMode.image); - }, - child: Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFD5D5D5).withAlpha(25), - offset: Offset(4, 2), - blurRadius: 4, - spreadRadius: 0, - ) - ], - ), - padding: EdgeInsets.all(12), - child: Image.asset( - "assets/image/icon_chat_photo.webp", - height: 24, - width: 24, - ), + behavior: HitTestBehavior.opaque, + onTap: () { + getImageOrVideo(GalleryMode.image); + }, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFD5D5D5).withAlpha(25), + offset: Offset(4, 2), + blurRadius: 4, + spreadRadius: 0, + ) + ], ), - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - "相册", - style: TextStyle( - fontSize: 12.sp, - color: Color(0xFFA29E9E), - fontWeight: MyFontWeight.regular, - ), + padding: EdgeInsets.all(12), + child: Image.asset( + "assets/image/icon_chat_photo.webp", + height: 24, + width: 24, + ), + ), + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + "相册", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFA29E9E), + fontWeight: MyFontWeight.regular, ), ), - ], - ), + ), + ], + ), ), ], ), @@ -1448,5 +1493,4 @@ class _ChatDetailsPage extends State print(tex); Clipboard.setData(ClipboardData(text: tex)); } - } diff --git a/lib/im/chat_friend_group.dart b/lib/im/chat_friend_group.dart index 5e5657d5..2508a872 100644 --- a/lib/im/chat_friend_group.dart +++ b/lib/im/chat_friend_group.dart @@ -4,18 +4,14 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../generated/l10n.dart'; -import '../../utils/font_weight.dart'; -import '../main.dart'; import '../retrofit/data/base_data.dart'; import '../retrofit/data/social_info.dart'; -import '../utils/event_type.dart'; import '../view_widget/my_tab.dart'; import 'im_view/custom_underline_tabIndicator.dart'; import 'im_view/friend_groip_list.dart'; diff --git a/lib/im/chat_setting.dart b/lib/im/chat_setting.dart index 5f8654ce..cd812253 100644 --- a/lib/im/chat_setting.dart +++ b/lib/im/chat_setting.dart @@ -4,13 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/im/database/message.dart'; -import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../generated/l10n.dart'; import '../../utils/font_weight.dart'; import '../main.dart'; import '../retrofit/data/im_user.dart'; diff --git a/lib/im/contact_share.dart b/lib/im/contact_share.dart index b144e1e4..bbcc991c 100644 --- a/lib/im/contact_share.dart +++ b/lib/im/contact_share.dart @@ -8,8 +8,6 @@ import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:flutter/cupertino.dart'; -import '../../generated/l10n.dart'; -import '../../retrofit/data/base_data.dart'; import '../../utils/font_weight.dart'; class ContactsShare extends StatefulWidget { diff --git a/lib/im/im_search.dart b/lib/im/im_search.dart index 7eeef2d1..3458587d 100644 --- a/lib/im/im_search.dart +++ b/lib/im/im_search.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; From 6a740723f6638cf7f3a77074f1e930f5fee390c6 Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 18 Oct 2024 11:50:55 +0800 Subject: [PATCH 79/81] config xiugai --- lib/constant.dart | 13 +- lib/im/chat_details_page.dart | 437 ++++++++------------- lib/retrofit/data/address.dart | 4 +- lib/retrofit/min_api.dart | 2 +- lib/retrofit/min_api.g.dart | 4 +- lib/store/store_view/shop_goods.dart | 14 +- lib/store/store_view/store_order_list.dart | 2 +- 7 files changed, 197 insertions(+), 279 deletions(-) diff --git a/lib/constant.dart b/lib/constant.dart index 5acc9557..3494c953 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -1,21 +1,25 @@ import 'package:flutter/foundation.dart'; +/// 图片消息的图片URL的HOST const String chatImageHost = "http://skk8mlm5b.hn-bkt.clouddn.com/"; -//47.93.216.24:9090 线上 192.168.10.129:9090 测试 -const String socketHost = kDebugMode ? '192.168.10.129' : '47.93.216.24'; +/// socket的host和port端口 +//47.93.216.24:9090 线上 192.168.10.200/192.168.10.129:9090 测试 +const String socketHost = kDebugMode ? '192.168.10.200' : '47.93.216.24'; const num socketPort = 9090; const ipBaseUrl = "http://whois.pconline.com.cn"; +/// 小程序接口的请求地址 const localMiniBaseUrl = "http://192.168.10.54:8765/app/";///本地 const serviceMiniBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线上 +/// app接口的请求地址 const localBaseUrl = "http://192.168.10.54:8766/app/";///本地 const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线上 - +/// 对list进行分组 Map> groupBy(Iterable values, S Function(T) key) { var map = >{}; for (var element in values) { @@ -24,6 +28,7 @@ Map> groupBy(Iterable values, S Function(T) key) { return map; } +/// 对list进行分组计数 Map groupCount(Map> values) { var map = {}; for (var element in values.keys) { @@ -32,6 +37,7 @@ Map groupCount(Map> values) { return map; } +/// 对list进行分组并取最大值 Map groupItem(Map> values, {int Function(T) key}) { var map = {}; for (var element in values.keys) { @@ -43,6 +49,7 @@ Map groupItem(Map> values, {int Function(T) key}) { return map; } +/// 最大值 T max(Iterable list, int Function(T) key) { T tt; for (T t in list) { diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 6f47ade2..6b04d442 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -11,11 +11,13 @@ import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:huixiang/im/database/message.dart'; +import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/main.dart'; import 'package:huixiang/retrofit/data/im_user.dart'; import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/my_appbar.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_pickers/image_pickers.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; @@ -69,6 +71,7 @@ class _ChatDetailsPage extends State String selfUserId = ""; ImUser _toUser; String conversation; + List imageUrl = []; @override void onMessage(txt) { @@ -94,18 +97,23 @@ class _ChatDetailsPage extends State socketClient.addCallback(_toUser.mid, (Message message) { messages.insert(0, message); + if (message.msgType == MsgType.VIDEO.value || message.msgType == MsgType.IMAGE.value) { + imageUrl.add(message.attach); + } messageShowTime().then((value) { refreshState(); jumpToBottom(); }); }); + + messageImageUrl(); + refreshState(); jumpToBottom(); } Future refresh() async { - List messagePage = - await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); + List messagePage = await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); debugPrint("refresh-message-list: ${messagePage.length} page: $page"); if (messagePage.isEmpty) { refreshController.loadNoData(); @@ -123,6 +131,11 @@ class _ChatDetailsPage extends State return Future.value(); } + /// 消息中的图片 + messageImageUrl() { + imageUrl = messages.where((e) => e.msgType == MsgType.VIDEO.value || e.msgType == MsgType.IMAGE.value).map((e) => e.attach).toList(); + } + Future messageShowTime() async { List messagePages = await hxDatabase.queryTList(conversation); for (var value in messages) { @@ -601,8 +614,8 @@ class _ChatDetailsPage extends State GlobalKey _buttonKey = GlobalKey(); return Container( padding: EdgeInsets.only( - top: 16.h, - bottom: 16.h, + top: 16, + bottom: 16, ), child: Column( children: [ @@ -684,126 +697,7 @@ class _ChatDetailsPage extends State alignment: Alignment.centerLeft, child: GestureDetector( onLongPress: () { - RenderBox renderBox = _buttonKey.currentContext - .findRenderObject() as RenderBox; - Offset buttonPosition = - renderBox.localToGlobal(Offset.zero); - Size buttonSize = renderBox.size; - - showMenu( - context: context, - //去除阴影 - elevation: 0, - color: Colors.transparent, - position: RelativeRect.fromLTRB( - buttonPosition.dx + - (buttonSize.width - 180.w) / 2, // 居中对齐 - buttonPosition.dy - 68, - buttonPosition.dx + buttonSize.width, - buttonPosition.dy), - - /// 设置弹出菜单的显示位置 - items: [ - PopupMenuItem( - value: 1, - padding: EdgeInsets.zero, - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - padding: EdgeInsets.only(bottom: 13.h), - child: Container( - width: 180.w, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: - BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric( - horizontal: 32.w, vertical: 7.5.h), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - this.copy(messages[position] - .content); - Navigator.pop(context); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: - MyFontWeight.regular, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () async { - await hxDatabase.deleteByMsgId( - messages[position] - .id - .toString()); - messages.removeAt(position); - Navigator.pop(context); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: - MyFontWeight.regular, - ), - ), - ], - ), - ) - ], - ), - ), - ), - Image.asset( - "assets/image/icon_copy_j.webp", - height: 17, - width: 17, - ), - ], - ), - ), - ], - ); + showMessageMenu(context, _buttonKey, messages[position]); }, child: Container( decoration: BoxDecoration( @@ -881,130 +775,7 @@ class _ChatDetailsPage extends State alignment: Alignment.centerRight, child: InkWell( onLongPress: () { - RenderBox renderBox = _buttonKey.currentContext - .findRenderObject() as RenderBox; - Offset buttonPosition = - renderBox.localToGlobal(Offset.zero); - Size buttonSize = renderBox.size; - - showMenu( - context: context, - //去除阴影 - elevation: 0, - color: Colors.transparent, - position: RelativeRect.fromLTRB( - buttonPosition.dx + - (buttonSize.width - 180.w) / 2, // 居中对齐 - buttonPosition.dy - 68, - buttonPosition.dx + buttonSize.width, - buttonPosition.dy), - - /// 设置弹出菜单的显示位置 - items: [ - PopupMenuItem( - value: 1, - padding: EdgeInsets.zero, - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Container( - padding: EdgeInsets.only(bottom: 13.h), - child: Container( - width: 180.w, - decoration: BoxDecoration( - color: Color(0xFF2A2A2A), - borderRadius: - BorderRadius.circular(6), - ), - padding: EdgeInsets.symmetric( - horizontal: 32.w, - vertical: 7.5.h), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - setState(() { - this.copy(messages[position] - .content); - Navigator.pop(context); - }); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_copy.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - "复制", - textAlign: - TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight - .regular, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () async { - await hxDatabase - .deleteByMsgId( - messages[position] - .id - .toString()); - messages.removeAt(position); - Navigator.pop(context); - }, - child: Column( - children: [ - Image.asset( - "assets/image/icon_chat_delete.webp", - height: 16, - width: 16, - ), - SizedBox( - height: 2.h, - ), - Text( - S.of(context).shanchu, - textAlign: - TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: MyFontWeight - .regular, - ), - ), - ], - ), - ) - ], - ), - ), - ), - Image.asset( - "assets/image/icon_copy_j.webp", - height: 17, - width: 17, - ), - ], - ), - ), - ], - ); + showMessageMenu(context, _buttonKey, messages[position]); }, child: Container( decoration: BoxDecoration( @@ -1035,7 +806,8 @@ class _ChatDetailsPage extends State ), ), ), - )), + ), + ), ), SizedBox( width: 12.w, @@ -1085,11 +857,16 @@ class _ChatDetailsPage extends State ), GestureDetector( onLongPress: () { - setState(() {}); + showMessageMenu(context, _buttonKey, messages[position]); + }, + onTap: () { + if (imageUrl.contains(messages[position].attach)) { + ImagePickers.previewImages(imageUrl, imageUrl.indexOf(messages[position].attach)); + } }, - onTap: () {}, child: FutureBuilder( future: fetchImageSize(messages[position]), + key: _buttonKey, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting || @@ -1098,6 +875,7 @@ class _ChatDetailsPage extends State File(messages[position].attach), width: 180.w, height: 200.h, + alignment: Alignment.centerLeft, fit: BoxFit.contain, ); } else { @@ -1105,6 +883,7 @@ class _ChatDetailsPage extends State File(messages[position].attach), width: snapshot.data.width, height: snapshot.data.height, + alignment: Alignment.centerLeft, fit: BoxFit.cover, ); } @@ -1119,25 +898,34 @@ class _ChatDetailsPage extends State /// self image 图片 if (isSelf && isImage) Padding( - padding: EdgeInsets.only(left: 39.w, right: 17.w, top: 20.h), + padding: EdgeInsets.only( + left: 39.w, + right: 17.w, + top: 20.h, + ), child: Row( children: [ Spacer(), GestureDetector( onLongPress: () { - setState(() {}); + showMessageMenu(context, _buttonKey, messages[position]); }, - onTap: () {}, - child: FutureBuilder( + onTap: () { + debugPrint("imageUrl: ${imageUrl}"); + if (imageUrl.contains(messages[position].attach)) { + ImagePickers.previewImages(imageUrl, imageUrl.indexOf(messages[position].attach)); + } + }, + child: FutureBuilder ( future: fetchImageSize(messages[position]), + key: _buttonKey, builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting || - snapshot.hasError) { + if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { return Image.file( File(messages[position].attach), width: 180.w, height: 200.h, + alignment: Alignment.centerRight, fit: BoxFit.contain, ); } else { @@ -1145,7 +933,8 @@ class _ChatDetailsPage extends State File(messages[position].attach), width: snapshot.data.width, height: snapshot.data.height, - fit: BoxFit.cover, + alignment: Alignment.centerRight, + fit: BoxFit.contain, ); } }, @@ -1186,13 +975,132 @@ class _ChatDetailsPage extends State ); } + /// 显示消息菜单 + showMessageMenu(context, _buttonKey, message) { + RenderBox renderBox = _buttonKey.currentContext.findRenderObject() as RenderBox; + Offset buttonPosition = renderBox.localToGlobal(Offset.zero); + Size buttonSize = renderBox.size; + showMenu( + context: context, ///去除阴影 + elevation: 0, + color: Colors.transparent, + position: RelativeRect.fromLTRB( + buttonPosition.dx + (buttonSize.width - 180.w) / 2, // 居中对齐 + buttonPosition.dy - 68, + buttonPosition.dx + buttonSize.width, + buttonPosition.dy, + ), + /// 设置弹出菜单的显示位置 + items: [ + PopupMenuItem( + value: 1, + padding: EdgeInsets.zero, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.only(bottom: 13.h), + child: Container( + width: 180.w, + decoration: BoxDecoration( + color: Color(0xFF2A2A2A), + borderRadius: BorderRadius.circular(6), + ), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 7.5.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() { + this.copy(message.content); + Navigator.pop(context); + }); + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_copy.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + "复制", + textAlign: + TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight + .regular, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () async { + await hxDatabase.deleteByMsgId("${message.id}"); + messages.remove(message); + Navigator.pop(context); + if (mounted) { + setState(() {}); + } + }, + child: Column( + children: [ + Image.asset( + "assets/image/icon_chat_delete.webp", + height: 16, + width: 16, + ), + SizedBox( + height: 2.h, + ), + Text( + S.of(context).shanchu, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: MyFontWeight.regular, + ), + ), + ], + ), + ), + ], + ), + ), + ), + Image.asset( + "assets/image/icon_copy_j.webp", + height: 17, + width: 17, + ), + ], + ), + ), + ], + ); + } + Future fetchImageSize(Message message) async { String imageLocalPath = message.attach; + String imageUrl = message.content; debugPrint("$imageLocalPath"); + debugPrint("$imageUrl"); File file = File(imageLocalPath); if (!(await file.exists())) { - await qiniu.downloadFile(message.content, savePath: imageLocalPath); + await qiniu.downloadFile(imageUrl, savePath: imageLocalPath); } Size size = Size.zero; @@ -1200,7 +1108,7 @@ class _ChatDetailsPage extends State if (sharedPre != null) { String sizeStr = sharedPre.getString(imageLocalPath); if (sizeStr != null && sizeStr != "") { - Map sizeMap = jsonDecode(sizeStr); + Map sizeMap = jsonDecode(sizeStr); size = Size(sizeMap["width"], sizeMap["height"]); if (size != Size.zero) { return size; @@ -1319,9 +1227,8 @@ class _ChatDetailsPage extends State GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - // _onMoreTap(); - // jumpToBottom(); - SmartDialog.showToast("暂不支持", alignment: Alignment.center); + _onMoreTap(); + jumpToBottom(); }, child: Container( padding: EdgeInsets.only(left: 8.w, right: 19.w), @@ -1490,7 +1397,7 @@ class _ChatDetailsPage extends State /// 复制文本 copy(String tex) { - print(tex); + debugPrint(tex); Clipboard.setData(ClipboardData(text: tex)); } } diff --git a/lib/retrofit/data/address.dart b/lib/retrofit/data/address.dart index 8065d54d..35dff28a 100644 --- a/lib/retrofit/data/address.dart +++ b/lib/retrofit/data/address.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; + class Address { Address(); @@ -20,7 +22,7 @@ class Address { factory Address.fromJson(Map json) => Address() ..address = json['address'] as String ..area = json['area'] as String - ..city = (json['city'] is List) ? [] : json['city'] as String + ..city = (json['city'] is List) ? jsonEncode(json['city']) : json['city'] as String ..cityInfo = json['cityInfo'] as String ..id = json['id'] as String ..isDefault = json['isDefault'] as bool diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index 011cceca..1f751416 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -31,7 +31,7 @@ part 'min_api.g.dart'; ///调用小程序的接口 -@RestApi(baseUrl: localBaseUrl) +@RestApi(baseUrl: localMiniBaseUrl) abstract class MinApiService { factory MinApiService( Dio dio, { diff --git a/lib/retrofit/min_api.g.dart b/lib/retrofit/min_api.g.dart index 7397ed25..fa2d54bf 100644 --- a/lib/retrofit/min_api.g.dart +++ b/lib/retrofit/min_api.g.dart @@ -9,7 +9,7 @@ part of 'min_api.dart'; class _MinApiService implements MinApiService { _MinApiService(this._dio, {this.baseUrl}) { ArgumentError.checkNotNull(_dio, '_dio'); - baseUrl ??= kReleaseMode?serviceBaseUrl:localBaseUrl; + baseUrl ??= kReleaseMode?serviceMiniBaseUrl:localMiniBaseUrl; } final Dio _dio; @@ -113,7 +113,7 @@ class _MinApiService implements MinApiService { const _extra = {}; final queryParameters = {'tableId': tableId}; final _data = {}; - final _result = await _dio.request>('shoppingcart', + final _result = await _dio.request>('/shoppingcart', queryParameters: queryParameters, options: RequestOptions( method: 'GET', diff --git a/lib/store/store_view/shop_goods.dart b/lib/store/store_view/shop_goods.dart index 480f6cb9..30179be0 100644 --- a/lib/store/store_view/shop_goods.dart +++ b/lib/store/store_view/shop_goods.dart @@ -132,7 +132,7 @@ class _ShopGoods extends State { crossAxisAlignment: CrossAxisAlignment.start, children:[ Padding( - padding: EdgeInsets.only(right: 16.w,bottom:7.h), + padding: EdgeInsets.only(right: 16.w, bottom:7), child: Row( children: [ Expanded( @@ -167,7 +167,7 @@ class _ShopGoods extends State { ), ), Padding( - padding: EdgeInsets.only(right: 16.w,bottom:7.h), + padding: EdgeInsets.only(right: 16.w,bottom:7), child:Text( (widget.productListBean != null ? widget.productListBean.shortName @@ -207,10 +207,11 @@ class _ShopGoods extends State { ), Spacer(), ], - ):SizedBox(height:23.h), + ) : SizedBox(height:23), Row( children: [ - Expanded(child:Text( + Expanded( + child:Text( "¥${AppUtils.calculateDouble(double.tryParse(widget.isShopCart ? widget.shoppingCartSkuItemListBean.skuPrice : widget.productListBean.price) ?? 0)}", style: TextStyle( color: Color(0xFFFF4500), @@ -218,7 +219,8 @@ class _ShopGoods extends State { fontFamily: 'JDZhengHT', fontWeight: MyFontWeight.medium, ), - )), + ), + ), // SizedBox( // width:5.w, // ), @@ -587,7 +589,7 @@ class _ShopGoods extends State { ], ), SizedBox( - height: 4.h, + height: 4, ), ///VIP价格(暂时弃用) // if (widget.productListBean.vipPrice != null) diff --git a/lib/store/store_view/store_order_list.dart b/lib/store/store_view/store_order_list.dart index eba0c2f7..b007c17d 100644 --- a/lib/store/store_view/store_order_list.dart +++ b/lib/store/store_view/store_order_list.dart @@ -114,7 +114,7 @@ class _StoreOrderListPage extends State { // refreshController.refreshFailed(); SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), alignment: Alignment.center); - debugPrint(error); + debugPrint("${error.toString()}"); }); if (baseData != null && baseData.isSuccess) { // refreshController.refreshCompleted(); From 4e4d5b882813440b49ec23108745404dba070cde Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Dec 2024 12:34:36 +0800 Subject: [PATCH 80/81] config xiugai --- ios/Runner.xcodeproj/project.pbxproj | 27 ++++++++++++++++----------- lib/retrofit/min_api.dart | 4 ++-- lib/retrofit/min_api.g.dart | 2 +- lib/retrofit/retrofit_api.dart | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aa352e6d..b56b454d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -602,7 +602,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -653,7 +653,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.32; + MARKETING_VERSION = 3.2.33; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -667,11 +667,12 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -801,7 +802,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -852,7 +853,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.32; + MARKETING_VERSION = 3.2.33; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -866,11 +867,13 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -886,7 +889,8 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 9; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = YF3Q8DVP52; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -937,7 +941,7 @@ "$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/Runner/baidu", ); - MARKETING_VERSION = 3.2.32; + MARKETING_VERSION = 3.2.33; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -951,11 +955,12 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/lib/retrofit/min_api.dart b/lib/retrofit/min_api.dart index 1f751416..d2cb39a2 100644 --- a/lib/retrofit/min_api.dart +++ b/lib/retrofit/min_api.dart @@ -31,7 +31,7 @@ part 'min_api.g.dart'; ///调用小程序的接口 -@RestApi(baseUrl: localMiniBaseUrl) +@RestApi(baseUrl: kReleaseMode ? serviceMiniBaseUrl : localMiniBaseUrl) abstract class MinApiService { factory MinApiService( Dio dio, { @@ -112,7 +112,7 @@ abstract class MinApiService { ); if (kReleaseMode) { - baseUrl = serviceBaseUrl; + baseUrl = serviceMiniBaseUrl; } return _MinApiService(dio, baseUrl: baseUrl); } diff --git a/lib/retrofit/min_api.g.dart b/lib/retrofit/min_api.g.dart index fa2d54bf..1fa5c5b6 100644 --- a/lib/retrofit/min_api.g.dart +++ b/lib/retrofit/min_api.g.dart @@ -9,7 +9,7 @@ part of 'min_api.dart'; class _MinApiService implements MinApiService { _MinApiService(this._dio, {this.baseUrl}) { ArgumentError.checkNotNull(_dio, '_dio'); - baseUrl ??= kReleaseMode?serviceMiniBaseUrl:localMiniBaseUrl; + baseUrl ??= kReleaseMode ? serviceMiniBaseUrl : localMiniBaseUrl; } final Dio _dio; diff --git a/lib/retrofit/retrofit_api.dart b/lib/retrofit/retrofit_api.dart index e51600fb..a0abde14 100644 --- a/lib/retrofit/retrofit_api.dart +++ b/lib/retrofit/retrofit_api.dart @@ -72,7 +72,7 @@ part 'retrofit_api.g.dart'; -@RestApi(baseUrl: localBaseUrl) +@RestApi(baseUrl: kReleaseMode ? serviceBaseUrl : localBaseUrl) abstract class ApiService { factory ApiService( Dio dio, { From 118336d76d152da457c6d473ebd296ca8b54805a Mon Sep 17 00:00:00 2001 From: zsw Date: Fri, 20 Dec 2024 14:17:31 +0800 Subject: [PATCH 81/81] =?UTF-8?q?=E9=87=8D=E8=BF=9E=E5=BB=B6=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/im/SocketClient.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index ee04f98c..8a1686e2 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -149,7 +149,9 @@ class SocketClient { reconnect() { dispose(); - connect(); + Future.delayed(Duration(milliseconds: 3000), () { + connect(); + }); } Map callbacks = {};