diff --git a/analysis_options.yaml b/analysis_options.yaml index d4e0f0c..d2a7504 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + non_const_call_to_literal_constructor: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9f0155d..1a22b91 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -24,6 +24,11 @@ PODS: - Flutter - FlutterMacOS - HydraAsync (2.0.6) + - image_cropper (0.0.4): + - Flutter + - TOCropViewController (~> 2.7.4) + - image_gallery_saver_plus (0.0.1): + - Flutter - image_picker_ios (0.0.1): - Flutter - install_plugin (2.0.0): @@ -68,6 +73,9 @@ PODS: - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS - tencent_cloud_chat_push (8.6.7019): - Flutter - TIMPush (= 8.6.7019) @@ -78,6 +86,7 @@ PODS: - TXIMSDK_Plus_iOS_XCFramework (~> 8.6.7019) - TIMPush (8.6.7019): - TXIMSDK_Plus_iOS_XCFramework (>= 8.6.7019) + - TOCropViewController (2.7.4) - TXIMSDK_Plus_iOS_XCFramework (8.6.7019) - url_launcher_ios (0.0.1): - Flutter @@ -102,6 +111,8 @@ DEPENDENCIES: - flutter_upgrader (from `.symlinks/plugins/flutter_upgrader/ios`) - fluwx (from `.symlinks/plugins/fluwx/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - image_cropper (from `.symlinks/plugins/image_cropper/ios`) + - image_gallery_saver_plus (from `.symlinks/plugins/image_gallery_saver_plus/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - install_plugin (from `.symlinks/plugins/install_plugin/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) @@ -112,6 +123,7 @@ DEPENDENCIES: - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - record_ios (from `.symlinks/plugins/record_ios/ios`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - tencent_cloud_chat_push (from `.symlinks/plugins/tencent_cloud_chat_push/ios`) - tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -128,6 +140,7 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - TIMPush + - TOCropViewController - TXIMSDK_Plus_iOS_XCFramework - WechatOpenSDK-XCFramework @@ -148,6 +161,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluwx/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" + image_cropper: + :path: ".symlinks/plugins/image_cropper/ios" + image_gallery_saver_plus: + :path: ".symlinks/plugins/image_gallery_saver_plus/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" install_plugin: @@ -168,6 +185,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/photo_manager/ios" record_ios: :path: ".symlinks/plugins/record_ios/ios" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" tencent_cloud_chat_push: :path: ".symlinks/plugins/tencent_cloud_chat_push/ios" tencent_cloud_chat_sdk: @@ -193,6 +212,8 @@ SPEC CHECKSUMS: fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063 + image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 + image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a install_plugin: e17e38d6f504857748a3ec1299d8a2bbeeeea854 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 @@ -207,9 +228,11 @@ SPEC CHECKSUMS: record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 tencent_cloud_chat_push: f87ae58098c2062b06e81f39fc53afc528395916 tencent_cloud_chat_sdk: 0a406f1854a65aad2f853494c02a2e084a027ab2 TIMPush: d0dfe96355ee413a7cacb2576f8aaa66f6073ab2 + TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 TXIMSDK_Plus_iOS_XCFramework: cb54f7de6e30e1368c6831c6eff31c25393bbb98 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b diff --git a/lib/IM/im_message.dart b/lib/IM/im_message.dart index 071d14d..49c2cad 100644 --- a/lib/IM/im_message.dart +++ b/lib/IM/im_message.dart @@ -27,7 +27,7 @@ class IMMessage { bool isExcludedFromUnreadCount = false, }) async { // 必须且只能设置一个:toUserID(单聊)或 groupID(群聊) - if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) { + if ((toUserID == null && groupID == null) || (toUserID == '' && groupID == '')) { return ImResult( success: false, code: -1, @@ -41,7 +41,7 @@ class IMMessage { V2TimValueCallback sendRes; // final controller = Get.find(); // 单聊 - if (toUserID != null) { + if (toUserID != null && toUserID.isNotEmpty) { final myInfo = Get.find(); logger.w('启用默认title:${myInfo.nickname.value}'); OfflinePushInfo offlinePushInfo = OfflinePushInfo( diff --git a/lib/components/my_qrcode.dart b/lib/components/my_qrcode.dart new file mode 100644 index 0000000..c9710e4 --- /dev/null +++ b/lib/components/my_qrcode.dart @@ -0,0 +1,129 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; +import 'package:loopin/IM/im_friend_listeners.dart'; +import 'package:loopin/components/my_toast.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/scan_code_type.dart'; +import 'package:pretty_qr_code/pretty_qr_code.dart'; + +class MyQrcode extends StatelessWidget { + MyQrcode({super.key}); + + final controller = Get.find(); + + // GlobalKey 用于截图 Widget + final GlobalKey _qrKey = GlobalKey(); + + /// 将 Widget 渲染为 Uint8List + Future capturePng() async { + try { + final boundary = _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + if (boundary == null) return null; + + final image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + return byteData?.buffer.asUint8List(); + } catch (e) { + logger.e("截图失败: $e"); + } + return null; + } + + /// 保存二维码到相册 + Future saveQrToGallery() async { + logger.w('长安了'); + final pngBytes = await capturePng(); + if (pngBytes == null) return; + + final result = await ImageGallerySaverPlus.saveImage( + pngBytes, + quality: 100, + name: "my_qr_${DateTime.now().millisecondsSinceEpoch}", + ); + + logger.w("保存结果: $result"); + MyToast().tip( + title: '图片已保存', + position: 'top', + type: 'success', + ); + } + + @override + Widget build(BuildContext context) { + final userID = controller.userID.value; + final faceUrl = controller.faceUrl.value; + ImageProvider face; + if (faceUrl.isEmpty) { + face = AssetImage('assets/images/logo/logo.png'); + } else { + face = CachedNetworkImageProvider(faceUrl); + } + + return GestureDetector( + onLongPress: () { + showModalBottomSheet( + context: Get.context!, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.save, color: Colors.black), + title: const Text('保存到相册', style: TextStyle(color: Colors.black)), + onTap: () async { + await saveQrToGallery(); + Get.back(); + }, + ), + ], + ), + ); + }, + ); + }, + child: Center( + child: Container( + alignment: Alignment.topCenter, + decoration: BoxDecoration( + color: Colors.transparent, + ), + child: RepaintBoundary( + key: _qrKey, + child: PrettyQrView.data( + errorCorrectLevel: QrErrorCorrectLevel.H, // 高容错 + data: '${QrTypeCode.hym}$userID', + decoration: PrettyQrDecoration( + background: Colors.transparent, + shape: const PrettyQrShape.custom( + PrettyQrSmoothSymbol(color: FStyle.primaryColor), + finderPattern: PrettyQrSmoothSymbol(color: FStyle.primaryColor), + alignmentPatterns: PrettyQrSmoothSymbol(color: FStyle.primaryColor), + ), + image: PrettyQrDecorationImage( + image: face, + scale: 0.3, // 默认 + opacity: 1, + padding: EdgeInsets.all(8.0), // 图片与二维码的边距 + ), + quietZone: const PrettyQrQuietZone.modules(2), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/network_or_asset_image.dart b/lib/components/network_or_asset_image.dart index 00ca5b7..c7362e4 100644 --- a/lib/components/network_or_asset_image.dart +++ b/lib/components/network_or_asset_image.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class NetworkOrAssetImage extends StatelessWidget { @@ -21,31 +22,23 @@ class NetworkOrAssetImage extends StatelessWidget { final isNetwork = imageUrl != null && imageUrl!.isNotEmpty && (imageUrl!.startsWith('http://') || imageUrl!.startsWith('https://')); if (isNetwork) { - return Image.network( - imageUrl!, + return CachedNetworkImage( + imageUrl: imageUrl!, width: width, height: height, fit: fit, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - // 显示占位 - return Image.asset( - placeholderAsset.isEmpty ? 'assets/images/avatar/default.png' : placeholderAsset, - width: width, - height: height, - fit: fit, - ); - }, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - placeholderAsset, - width: width, - height: height, - fit: fit, - ); - }, + placeholder: (context, url) => Image.asset( + placeholderAsset.isEmpty ? 'assets/images/avatar/default.png' : placeholderAsset, + width: width, + height: height, + fit: fit, + ), + errorWidget: (context, url, error) => Image.asset( + placeholderAsset, + width: width, + height: height, + fit: fit, + ), ); } else { return Image.asset( diff --git a/lib/controller/shop_index_controller.dart b/lib/controller/shop_index_controller.dart index a094c1b..49270fd 100644 --- a/lib/controller/shop_index_controller.dart +++ b/lib/controller/shop_index_controller.dart @@ -39,14 +39,14 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta RxInt currentTabIndex = 0.obs; /// 初始化 Tab 分类 - void initTabs({required TickerProvider vsync}) async { + void initTabs() async { // 释放旧的 ScrollController tabs.forEach((_, state) => state.scrollController.dispose()); tabs.clear(); tabList.clear(); // 先释放旧 TabController(并移除监听) - tabController?.removeListener(_tabListener); - tabController?.dispose(); + // tabController?.removeListener(_tabListener); + // tabController?.dispose(); // 赋值 tab 数据 final res = await Http.post(ShopApi.shopCategory, data: { @@ -70,9 +70,10 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta } // 创建新的 TabController - tabController = TabController(length: tabList.length, vsync: vsync); + // tabController = TabController(length: tabList.length, vsync: vsync); + // tabController = changeController; - tabController?.addListener(_tabListener); + // tabController?.addListener(_tabListener); // 初始化第一个 tab 的数据 if (tabList.isNotEmpty) { loadSwiperData(); @@ -80,6 +81,21 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta } } + /// 添加监听 + void addTabListener(TabController changedTab) { + // 先解绑这个 TabController 的旧 listener + _removeTabListener(changedTab); + // 绑定 listener + changedTab.addListener(_tabListener); + // 更新当前的 tabController + tabController = changedTab; + } + + /// 移除监听 + void _removeTabListener(TabController changedTab) { + changedTab.removeListener(_tabListener); + } + /// Tab 切换监听 void _tabListener() { if (!tabController!.indexIsChanging) { diff --git a/lib/main.dart b/lib/main.dart index 3b49e7e..09d3c7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -99,7 +99,7 @@ class App extends StatelessWidget { ], debugShowCheckedModeBanner: false, theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFFF9900)), + // colorScheme: ColorScheme.fromSeed(seedColor: FStyle.primaryColor), useMaterial3: true, ), home: const Layout(), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 909b6a0..726420e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1688,32 +1688,34 @@ class _ChatState extends State with SingleTickerProviderStateMixin { color: FStyle.primaryColor, elevation: 8, items: [ - PopupMenuItem( - value: 'remark', - child: Row( - children: [ - Icon(Icons.edit, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设置备注', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - PopupMenuItem( - value: 'not', - child: Row( - children: [ - Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设为免打扰', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), + // PopupMenuItem( + // value: 'remark', + // child: Row( + // children: [ + // Icon(Icons.edit, color: Colors.white, size: 18), + // SizedBox(width: 8), + // Text( + // '设置备注', + // style: TextStyle(color: Colors.white), + // ), + // ], + // ), + // ), + + // PopupMenuItem( + // value: 'not', + // child: Row( + // children: [ + // Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18), + // SizedBox(width: 8), + // Text( + // '设为免打扰', + // style: TextStyle(color: Colors.white), + // ), + // ], + // ), + // ), + PopupMenuItem( value: 'report', child: Row( @@ -1740,40 +1742,42 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ], ), ), - PopupMenuItem( - value: 'foucs', - child: Row( - children: [ - Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '取消关注', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), + // PopupMenuItem( + // value: 'foucs', + // child: Row( + // children: [ + // Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18), + // SizedBox(width: 8), + // Text( + // '取消关注', + // style: TextStyle(color: Colors.white), + // ), + // ], + // ), + // ), ], ); if (selected != null) { switch (selected) { - case 'remark': - print('点击了备注'); - setRemark(); - break; - case 'not': - print('点击了免打扰'); - break; + // case 'remark': + // print('点击了备注'); + // setRemark(); + // break; + + // case 'not': + // print('点击了免打扰'); + // break; + case 'report': print('点击了举报'); break; case 'block': print('点击了拉黑'); break; - case 'foucs': - print('点击了取关'); - break; + // case 'foucs': + // print('点击了取关'); + // break; } } }, diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart index 77343e5..78a437b 100644 --- a/lib/pages/chat/chat_group.dart +++ b/lib/pages/chat/chat_group.dart @@ -1056,6 +1056,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix res = await IMMessage().sendMessage( msg: message, groupID: arguments.groupID, + groupName: arguments.showName, ); if (res.success && res.data != null) { diff --git a/lib/pages/chat/index.dart b/lib/pages/chat/index.dart index 9ae8b0a..738c1ea 100644 --- a/lib/pages/chat/index.dart +++ b/lib/pages/chat/index.dart @@ -2,20 +2,22 @@ library; import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/global_badge.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/api/shop_api.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/scan_util.dart'; import 'package:loopin/models/conversation_type.dart'; -import 'package:loopin/api/shop_api.dart'; -import 'package:loopin/service/http.dart'; import 'package:loopin/models/conversation_view_model.dart'; +import 'package:loopin/pages/chat/menu/add_friend.dart'; +import 'package:loopin/service/http.dart'; import 'package:loopin/utils/index.dart'; -import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举 import 'package:loopin/utils/parse_message_summary.dart'; +import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举 import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart'; @@ -55,7 +57,8 @@ class ChatPageState extends State { await Future.delayed(Duration(seconds: 1)); setState(() {}); } - // 处理扫码结果 + + // 处理扫码结果 void handleScanResult(String code) { print('扫码结果11111111111111111111111:$code'); @@ -85,37 +88,37 @@ class ChatPageState extends State { } } - // 处理核销码 - void _handleVerificationCode (String value) async { + // 处理核销码 + void _handleVerificationCode(String value) async { print('处理核销码: $value'); // 带着核销码,跳转到商家的商品详情页面,引导商家去手动点击核销按钮 - Get.toNamed('/sellerOrder/detail', arguments: {'writeOffCodeId':value}); + Get.toNamed('/sellerOrder/detail', arguments: {'writeOffCodeId': value}); } // 处理好友码 void _handleFriendCode(String value) { print('处理好友码: $value'); // 模仿抖音,去个人页面手动点击关注 - Get.toNamed('/vloger', arguments: {'memberId':value}); + Get.toNamed('/vloger', arguments: {'memberId': value}); } // 处理推广码 - void _handlePromotionCode(String value)async { - try { - print('处理推广码111: $value'); - final res = await Http.post('${ShopApi.bindSpreadCodeId}', data: { - "socialCode": value - }); - if(res != null && res['code'] == 200){ - MyDialog.toast('推广码绑定失败', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); - Get.toNamed('/vloger', arguments: {'memberId':value}); - } + void _handlePromotionCode(String value) async { + try { + print('处理推广码111: $value'); + final res = await Http.post(ShopApi.bindSpreadCodeId, data: {"socialCode": value}); + if (res != null && res['code'] == 200) { + MyDialog.toast('推广码绑定失败', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); + Get.toNamed('/vloger', arguments: {'memberId': value}); + } } catch (e) { MyDialog.toast('推广码绑定失败'); } } + // 长按菜单 void showContextMenu(BuildContext context, ConversationViewModel item) { + return; bool isLeft = posDX > MediaQuery.of(context).size.width / 2 ? false : true; bool isTop = posDY > MediaQuery.of(context).size.height / 2 ? false : true; @@ -183,7 +186,7 @@ class ChatPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.grey[50], + // backgroundColor: Colors.grey[50], appBar: AppBar( forceMaterialTransparency: true, title: Row( @@ -299,6 +302,7 @@ class ChatPageState extends State { break; case 'friend': logger.w('点击了添加朋友'); + Get.to(() => AddFriend()); break; case 'scan': logger.w('点击了扫一扫'); @@ -408,6 +412,7 @@ class ChatPageState extends State { // logger.w(chatList[index].conversation.conversationGroupList); // logger.w(chatList[index].isCustomAdmin); // logger.w(chatList[index].conversation.recvOpt); + final item = chatList[index]; final bool quiet = [ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At].contains(chatList[index].conversation.recvOpt ?? 0) ? true : false; // 是否设置了免打扰 @@ -416,125 +421,155 @@ class ChatPageState extends State { chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0'; final placeholderAsset = chatList[index].conversation.type == 2 ? 'assets/images/group.png' : 'assets/images/avatar/default.png'; // logger.e(chatList[index].isCustomAdmin); - return Ink( - // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 - child: InkWell( - key: ValueKey(chatList[index].conversation.conversationID), - splashColor: Colors.grey[200], - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), - child: Row( - spacing: 10.0, - children: [ - // 头图 - ClipOval( - child: NetworkOrAssetImage( - imageUrl: chatList[index].faceUrl, - width: 50, - height: 50, - placeholderAsset: placeholderAsset, + return Slidable( + key: ValueKey(chatList[index].conversation.conversationID), + endActionPane: ActionPane( + motion: const DrawerMotion(), // 可以改成 StretchMotion 或 ScrollMotion ,DrawerMotion + children: [ + SlidableAction( + onPressed: (_) { + // + }, + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.delete, + label: '已读', + ), + if (!(isAdmin || isNoFriend)) + SlidableAction( + onPressed: (_) { + // + }, + backgroundColor: FStyle.primaryColor, + foregroundColor: Colors.white, + icon: quiet ? Icons.notifications_off : Icons.notifications, + label: quiet ? '取消' : '免打扰', + ), + ], + ), + child: Ink( + // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 + child: InkWell( + key: ValueKey(chatList[index].conversation.conversationID), + splashColor: Colors.grey[200], + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + spacing: 10.0, + children: [ + // 头图 + ClipOval( + child: NetworkOrAssetImage( + imageUrl: chatList[index].faceUrl, + width: 50, + height: 50, + placeholderAsset: placeholderAsset, + ), ), - ), - // 消息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + // 消息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 昵称 + Text( + chatList[index].conversation.showName ?? '未知', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: (isAdmin || isNoFriend) ? 20 : 16, + fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), + ), + const SizedBox(height: 2.0), + // 消息内容 + Text( + chatList[index].conversation.lastMessage != null + ? parseMessageSummary(chatList[index].conversation.lastMessage!) + : '', + style: const TextStyle(color: Colors.grey, fontSize: 13.0), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + ), + ], + ), + ), + // 右侧 + + Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - // 昵称 - Text( - chatList[index].conversation.showName ?? '未知', - maxLines: 1, - softWrap: false, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: (isAdmin || isNoFriend) ? 20 : 16, - fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), + Visibility( + visible: !(isAdmin || isNoFriend), + child: Text( + // 转成日期字符串显示 + // DateTime.fromMillisecondsSinceEpoch( + // (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000, + // ).toLocal().toString().substring(0, 16), // 简单截取年月日时分 + Utils.formatTime( + chatList[index].conversation.lastMessage!.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + ), ), - const SizedBox(height: 2.0), - // 消息内容 - Text( - chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '', - style: const TextStyle(color: Colors.grey, fontSize: 13.0), - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, + const SizedBox(height: 5.0), + // 数字角标 + // class ReceiveMsgOptType { + // 在线正常接收消息,离线时会进行 APNs 推送 + // static const int kTIMRecvMsgOpt_Receive = 0; + // 不会接收到消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Receive = 1; + // 在线正常接收消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Notify = 2; + // 在线接收消息,离线只接收 at 消息的推送 + // static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3; + // 在线和离线都只接收@消息 + // static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4; + // } + // 现阶段只允许设置为0和3 + Visibility( + visible: (chatList[index].conversation.unreadCount ?? 0) > 0, + child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0, color: quiet ? Colors.grey : Colors.red), ), ], ), - ), - // 右侧 - - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Visibility( - visible: !(isAdmin || isNoFriend), - child: Text( - // 转成日期字符串显示 - // DateTime.fromMillisecondsSinceEpoch( - // (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000, - // ).toLocal().toString().substring(0, 16), // 简单截取年月日时分 - Utils.formatTime(chatList[index].conversation.lastMessage!.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), - style: const TextStyle(color: Colors.grey, fontSize: 12.0), - ), + Visibility( + visible: (isAdmin || isNoFriend), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.blueGrey, + size: 14.0, ), - const SizedBox(height: 5.0), - // 数字角标 - // class ReceiveMsgOptType { - // 在线正常接收消息,离线时会进行 APNs 推送 - // static const int kTIMRecvMsgOpt_Receive = 0; - // 不会接收到消息,离线不会有推送通知 - // static const int kTIMRecvMsgOpt_Not_Receive = 1; - // 在线正常接收消息,离线不会有推送通知 - // static const int kTIMRecvMsgOpt_Not_Notify = 2; - // 在线接收消息,离线只接收 at 消息的推送 - // static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3; - // 在线和离线都只接收@消息 - // static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4; - // } - // 现阶段只允许设置为0和3 - Visibility( - visible: (chatList[index].conversation.unreadCount ?? 0) > 0, - child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0, color: quiet ? Colors.grey : Colors.red), - ), - ], - ), - Visibility( - visible: (isAdmin || isNoFriend), - child: const Icon( - Icons.arrow_forward_ios, - color: Colors.blueGrey, - size: 14.0, ), - ), - ], + ], + ), ), + onTap: () { + if (conversationTypeFromString(chatList[index].isCustomAdmin) != null) { + // 跳转对应的通知消息页 + logger.e(chatList[index].isCustomAdmin); + Get.toNamed('/${chatList[index].isCustomAdmin}', arguments: chatList[index].conversation); + } else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) { + // 跳转陌生人消息页面 + logger.e(chatList[index].conversation.conversationGroupList); + Get.toNamed('/noFriend'); + } else if (chatList[index].conversation.type == 2) { + // 跳转群聊 type=0非法,1=单聊,2=群聊 + Get.toNamed('/chatGroup', arguments: chatList[index].conversation); + } else { + // 会话id查询会话详情 + Get.toNamed('/chat', arguments: chatList[index].conversation); + } + }, + onTapDown: (TapDownDetails details) { + posDX = details.globalPosition.dx; + posDY = details.globalPosition.dy; + }, + onLongPress: () { + showContextMenu(context, chatList[index]); + }, ), - onTap: () { - if (conversationTypeFromString(chatList[index].isCustomAdmin) != null) { - // 跳转对应的通知消息页 - logger.e(chatList[index].isCustomAdmin); - Get.toNamed('/${chatList[index].isCustomAdmin}', arguments: chatList[index].conversation.lastMessage); - } else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) { - // 跳转陌生人消息页面 - logger.e(chatList[index].conversation.conversationGroupList); - Get.toNamed('/noFriend'); - } else if (chatList[index].conversation.type == 2) { - // 跳转群聊 type=0非法,1=单聊,2=群聊 - Get.toNamed('/chatGroup', arguments: chatList[index].conversation); - } else { - // 会话id查询会话详情 - Get.toNamed('/chat', arguments: chatList[index].conversation); - } - }, - onTapDown: (TapDownDetails details) { - posDX = details.globalPosition.dx; - posDY = details.globalPosition.dy; - }, - onLongPress: () { - showContextMenu(context, chatList[index]); - }, ), ); }, diff --git a/lib/pages/chat/menu/add_friend.dart b/lib/pages/chat/menu/add_friend.dart new file mode 100644 index 0000000..9ab4201 --- /dev/null +++ b/lib/pages/chat/menu/add_friend.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; +import 'package:loopin/IM/im_friend_listeners.dart'; +import 'package:loopin/components/my_qrcode.dart'; +import 'package:loopin/components/my_toast.dart'; +import 'package:loopin/components/scan_util.dart'; +import 'package:loopin/utils/scan_code_type.dart'; + +class AddFriend extends StatefulWidget { + const AddFriend({super.key}); + @override + State createState() => _AddFriendState(); +} + +class _AddFriendState extends State { + final TextEditingController txtcontroller = TextEditingController(); + final FocusNode focusNode = FocusNode(); + + @override + void dispose() { + txtcontroller.dispose(); + super.dispose(); + } + + void handleScanResult(String code) { + logger.w('扫码结果11111111111111111111111:$code'); + + // 使用外部枚举的方法检查扫码类型 + final scanType = ScanCodeType.fromCode(code); + if (scanType != null) { + final value = code.substring(scanType.prefix.length + 1); // 获取后缀值 + _processScanCode(scanType, value); + } else { + // 未知类型的码 + Get.snackbar('未知的二维码类型', '无法识别此二维码: $code'); + } + } + + // 处理不同类型的扫码结果 + void _processScanCode(ScanCodeType type, String value) { + switch (type) { + case ScanCodeType.verification: + _handleVerificationCode(value); + break; + case ScanCodeType.friend: + _handleFriendCode(value); + break; + case ScanCodeType.promotion: + _handlePromotionCode(value); + break; + } + } + + // 处理核销码 + void _handleVerificationCode(String value) async { + logger.w('处理核销码: $value'); + // 带着核销码,跳转到商家的商品详情页面,引导商家去手动点击核销按钮 + Get.toNamed('/sellerOrder/detail', arguments: {'writeOffCodeId': value}); + } + + // 处理好友码 + void _handleFriendCode(String value) { + logger.w('处理好友码: $value'); + // 模仿抖音,去个人页面手动点击关注 + final myID = Get.find().userID.value; + if (myID != value) { + Get.toNamed('/vloger', arguments: {'memberId': value}); + } else { + MyToast().tip(title: '这是给别人扫的,扫自己没用', position: 'top'); + } + } + + // 处理推广码 + void _handlePromotionCode(String value) { + logger.w('处理推广码: $value'); + Get.toNamed('/vloger', arguments: {'memberId': value}); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + focusNode.unfocus(); + }, + child: Scaffold( + appBar: AppBar( + title: const Text('添加好友'), + titleTextStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + centerTitle: true, + ), + body: ListView( + children: [ + // 搜索框 + Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + controller: txtcontroller, + focusNode: focusNode, + decoration: InputDecoration( + hintText: '请输入昵称', + prefixIcon: const Icon(Icons.search), + suffixIcon: TextButton( + child: Text('搜索'), + onPressed: () { + // 点击按钮时触发的逻辑 + final value = txtcontroller.text.trim(); + if (value.isNotEmpty) { + // 跳转搜索页 + focusNode.unfocus(); + } + }, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.grey[200], + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + focusNode.unfocus(); + // 跳转搜索页 + } + }, + ), + ), + + const Divider(), + + // 功能入口 + _buildEntry( + icon: Icons.qr_code_scanner, + title: "扫一扫", + onTap: () { + // Get.snackbar("扫一扫", "跳转到扫码页面"); + ScanUtil.openScanner(onResult: handleScanResult); + }, + ), + // _buildEntry( + // icon: Icons.contacts, + // title: "手机联系人", + // onTap: () {}, + // ), + // _buildEntry( + // icon: Icons.people_alt_outlined, + // title: "好友推荐", + // onTap: () {}, + // ), + + SizedBox(height: 20), + + MyQrcode(), + ], + ), + ), + ); + } + + Widget _buildEntry({ + required IconData icon, + required String title, + required VoidCallback onTap, + }) { + return ListTile( + leading: Icon(icon, color: Colors.blue), + title: Text(title), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: onTap, + ); + } +} diff --git a/lib/pages/chat/notify/interaction.dart b/lib/pages/chat/notify/interaction.dart index a0f6ba4..5db6f74 100644 --- a/lib/pages/chat/notify/interaction.dart +++ b/lib/pages/chat/notify/interaction.dart @@ -1,4 +1,4 @@ -/// 聊天首页模板 +/// 互动通知 library; import 'dart:convert'; diff --git a/lib/pages/chat/notify/newFoucs.dart b/lib/pages/chat/notify/newFoucs.dart index 7fa09b7..ce13bb6 100644 --- a/lib/pages/chat/notify/newFoucs.dart +++ b/lib/pages/chat/notify/newFoucs.dart @@ -1,28 +1,26 @@ -/// 关注列表 +/// 新关注通知 library; -import 'package:easy_refresh/easy_refresh.dart'; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/api/video_api.dart'; import 'package:loopin/behavior/custom_scroll_behavior.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/models/conversation_type.dart'; +import 'package:loopin/service/http.dart'; import 'package:loopin/styles/index.dart'; import 'package:loopin/utils/index.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; -class UserWithFollow { - final V2TimUserFullInfo userInfo; - int followType; - - UserWithFollow({ - required this.userInfo, - this.followType = 0, - }); -} +// interactionComment, //互动->评论 +// interactionAt, //互动->视频评论中的@ +// interactionLike, //互动->点赞 +// interactionReply, //互动->评论回复 class Newfoucs extends StatefulWidget { const Newfoucs({super.key}); @@ -34,78 +32,71 @@ class Newfoucs extends StatefulWidget { class NewfoucsState extends State with SingleTickerProviderStateMixin { bool isLoading = false; // 是否在加载中 bool hasMore = true; // 是否还有更多数据 + final RxBool _throttleFlag = false.obs; // 滚动节流锁 + final ScrollController chatController = ScrollController(); String page = ''; - List dataList = []; ///------------------- + V2TimConversation? conv; + RxList msgList = [].obs; @override void initState() { super.initState(); - getData(); + if (Get.arguments != null && Get.arguments is V2TimConversation) { + // 如果有参数 + conv = Get.arguments as V2TimConversation; + logger.e('lastmsg:$conv'); + + final lastmsg = conv?.lastMessage; + if (lastmsg != null) { + msgList.add(lastmsg); + } + } + chatController.addListener(() { + if (_throttleFlag.value) return; + if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) { + _throttleFlag.value = true; + getMsgData().then((_) { + // 解锁 + Future.delayed(Duration(milliseconds: 1000), () { + _throttleFlag.value = false; + }); + }); + } + }); } - // 分页获取陌关注列表数据 - Future getData() async { - /// 0:不是好友也没有关注 - /// 1:你关注了对方(单向) - /// 2:对方关注了你(单向) - /// 3:互相关注(双向好友) - final res = await ImService.instance.getMyFollowingList( - nextCursor: page, + @override + void dispose() { + super.dispose(); + chatController.dispose(); + } + + // 分页获取全部数据 + Future getMsgData() async { + // 获取最旧一条消息作为游标 + V2TimMessage? lastRealMsg; + lastRealMsg = msgList.last; + final res = await ImService.instance.getHistoryMessageList( + userID: ConversationType.newFocus.name, // userID为固定的newFocus + lastMsg: lastRealMsg, ); if (res.success && res.data != null) { - logger.i('获取成功:${res.data!.nextCursor}'); - final userInfoList = res.data!.userFullInfoList ?? []; - // 构建数据 - List wrappedList = userInfoList.map((u) { - return UserWithFollow(userInfo: u); - }).toList(); - // 获取id - final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); - if (userIDList.isNotEmpty) { - final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); - if (shiRes.success && shiRes.data != null) { - final shipResData = shiRes.data!; - for (final uwf in wrappedList) { - final userID = uwf.userInfo.userID; - if (userID != null) { - // 查找对应关系 - final match = shipResData.firstWhere( - (e) => e.userID == userID, - orElse: () => V2TimFollowTypeCheckResult(userID: ''), - ); - if (match.userID?.isNotEmpty ?? false) { - uwf.followType = match.followType ?? 0; - } - } - } - } + msgList.addAll(res.data!); + logger.e(msgList); + if (res.data!.isEmpty) { + hasMore = false; } - final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; - if (isFinished) { - setState(() { - hasMore = false; - }); - // 加载没数据了 - page = ''; - } else { - page = res.data!.nextCursor ?? ''; - } - logger.i('获取数据成功:$userInfoList'); - setState(() { - dataList.addAll(wrappedList); - }); + logger.i('聊天数据加载成功'); } else { - logger.e('获取数据失败:${res.desc}'); + logger.e('聊天数据加载失败:${res.desc}'); } } // 下拉刷新 Future handleRefresh() async { - dataList.clear(); - page = ''; - getData(); + await Future.delayed(Duration(seconds: 5)); setState(() {}); } @@ -117,15 +108,28 @@ class NewfoucsState extends State with SingleTickerProviderStateMixin centerTitle: true, forceMaterialTransparency: true, bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), + preferredSize: Size.fromHeight(1.0), child: Container( color: Colors.grey[300], height: 1.0, ), ), - title: const Text( - '关注', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + title: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + Text( + '新的关注', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ), actions: [], ), @@ -134,91 +138,103 @@ class NewfoucsState extends State with SingleTickerProviderStateMixin child: Column( children: [ Expanded( - child: EasyRefresh.builder( - callLoadOverOffset: 20, //触底距离 - callRefreshOverOffset: 20, // 下拉距离 - header: ClassicHeader( - dragText: '下拉刷新', - armedText: '释放刷新', - readyText: '加载中...', - processingText: '加载中...', - processedText: '加载完成', - failedText: '加载失败,请重试', - messageText: '最后更新于 %T', - ), - footer: ClassicFooter( - dragText: '加载更多', - armedText: '释放加载', - readyText: '加载中...', - processingText: '加载中...', - processedText: hasMore ? '加载完成' : '没有更多了~', - failedText: '加载失败,请重试', - messageText: '最后更新于 %T', - ), - onRefresh: () async { - await handleRefresh(); - }, - onLoad: () async { - if (hasMore) { - await getData(); - } - }, - childBuilder: (context, physics) { - return ListView.builder( - physics: physics, - itemCount: dataList.length, - itemBuilder: (context, index) { - final item = dataList[index]; - return Ink( - key: ValueKey(item.userInfo.userID), - child: Container( + child: RefreshIndicator( + backgroundColor: Colors.white, + color: Color(0xFFFF5000), + displacement: 10.0, + onRefresh: handleRefresh, + child: Obx(() { + return ListView.builder( + controller: chatController, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemCount: msgList.length, + itemBuilder: (context, index) { + //检测cloudCustomData + + //----正式数据 + V2TimMessage msg = msgList[index]; + V2TimCustomElem element = msgList[index].customElem!; + final cloudCustomData = msgList[index].cloudCustomData; + logger.w(cloudCustomData); + final desc = msgList[index].customElem!.desc!; + String? jsonData = msgList[index].customElem!.data; + jsonData = (jsonData == null || jsonData.isEmpty) ? '{"faceUrl":"","nickName":"data为空","userID":"213213"}' : jsonData; + + final item = jsonDecode(jsonData ?? '{"faceUrl":"","nickName":"测试昵称","userID":"213213"}'); + + logger.w(element.toJson()); + + // ----测试数据 + // final jsonData = '{"faceUrl":"","nickName":"测试昵称","userID":"213213"}'; + // final item = jsonDecode(jsonData); // 数据 + // final desc = '测试desc'; + // final cloudCustomData = 'interactionLike'; + // V2TimMessage element = V2TimMessage(elemType: 2, isRead: index > 2 ? true : false); + // ----------- + return Container( + width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + spacing: 10.0, children: [ - // 左侧部分(头像 + 昵称 + 描述) + // 头像 + InkWell( + onTap: () async { + // 点击头像转到对方主页 + // 先获取视频详情 + // 如果cloudCustomData是interactionComment,interactionAt,interactionReply,传参时带上评论id, + // + final res = await Http.get('${VideoApi.detail}/${item['vlogID']}'); + Get.toNamed('/vloger', arguments: res['data']); + // Get.toNamed('/vloger'); + }, + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: item['faceUrl'], + width: 50, + height: 50, + ), + ), + ), + + // 消息 Expanded( child: InkWell( - onTap: () { - Get.toNamed( - '/vloger', - arguments: item.userInfo.userID, - ); + onTap: () async { + // 点击头像转到对方主页 + // 先获取视频详情 + // 如果cloudCustomData是interactionComment,interactionAt,interactionReply,传参时带上评论id, + // + final res = await Http.get('${VideoApi.detail}/${item['vlogID']}'); + Get.toNamed('/vloger', arguments: res['data']); + // Get.toNamed('/vloger'); }, - child: Row( - children: [ - ClipOval( - child: NetworkOrAssetImage( - imageUrl: item.userInfo.faceUrl, - width: 50, - height: 50, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 昵称 + Text( + item['nickName'] ?? '未知', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, ), ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - ), - ), - if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ - const SizedBox(height: 2.0), - Text( - item.userInfo.selfSignature!, - style: const TextStyle( - color: Colors.grey, - fontSize: 13.0, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ], + const SizedBox(height: 2.0), + // 描述内容 + Text( + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + desc, + // '很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容', + ), + Text( + Utils.formatTime(msg.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, ), ), ], @@ -226,75 +242,34 @@ class NewfoucsState extends State with SingleTickerProviderStateMixin ), ), - SizedBox(width: 10), - - // 右侧按钮 - TextButton( - style: TextButton.styleFrom( - backgroundColor: item.followType == 3 ? Colors.grey : FStyle.primaryColor, - minimumSize: const Size(70, 32), - padding: const EdgeInsets.symmetric(horizontal: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + // 右侧 + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Visibility( + visible: true, + // 视频首图 + child: NetworkOrAssetImage( + imageUrl: item['firstFrameImg'], + placeholderAsset: 'assets/images/bk.jpg', + width: 40, + height: 60, + ), ), - ), - onPressed: () async { - final ctl = Get.find(); - final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); - int realFollowType = 0; - if (checkRes.success && checkRes.data != null) { - realFollowType = checkRes.data!.first.followType ?? 0; - if ([1, 3].contains(realFollowType)) { - // 取关 - final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); - if (unRes.success) { - setState(() { - item.followType = 2; - }); - ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); - } - } else { - // 关注 - final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); - if (res.success) { - setState(() { - item.followType = realFollowType == 0 - ? 1 - : realFollowType == 2 - ? 3 - : 0; - }); - final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); - if (chatRes.success) { - final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); - if (res.success) { - V2TimConversation conversation = res.data; - if (conversation.conversationGroupList?.isNotEmpty ?? false) { - await ImService.instance.deleteConversationsFromGroup( - groupName: conversation.conversationGroupList!.first!, - conversationIDList: [conversation.conversationID], - ); - ctl.updateNoFriendMenu(); - } - } - } - } - } - } - }, - child: Text( - Utils.getTipText(item.followType), - style: const TextStyle(color: Colors.white, fontSize: 14), - ), + const SizedBox(width: 5.0), + // 角标 + Visibility( + visible: !(msg.isRead ?? true), + child: FStyle.badge(0, isdot: true), + ), + ], ), ], ), - ), - ); - }, - ); - }, - ), + ); + }, + ); + })), ), ], ), diff --git a/lib/pages/chat/notify/order.dart b/lib/pages/chat/notify/order.dart index e69de29..439a906 100644 --- a/lib/pages/chat/notify/order.dart +++ b/lib/pages/chat/notify/order.dart @@ -0,0 +1,304 @@ +/// 订单通知列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class UserWithFollow { + final V2TimUserFullInfo userInfo; + int followType; + + UserWithFollow({ + required this.userInfo, + this.followType = 0, + }); +} + +class Order extends StatefulWidget { + const Order({super.key}); + + @override + State createState() => OrderState(); +} + +class OrderState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取陌关注列表数据 + Future getData() async { + /// 0:不是好友也没有关注 + /// 1:你关注了对方(单向) + /// 2:对方关注了你(单向) + /// 3:互相关注(双向好友) + final res = await ImService.instance.getMyFollowingList( + nextCursor: page, + ); + if (res.success && res.data != null) { + logger.i('获取成功:${res.data!.nextCursor}'); + final userInfoList = res.data!.userFullInfoList ?? []; + // 构建数据 + List wrappedList = userInfoList.map((u) { + return UserWithFollow(userInfo: u); + }).toList(); + // 获取id + final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); + if (userIDList.isNotEmpty) { + final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); + if (shiRes.success && shiRes.data != null) { + final shipResData = shiRes.data!; + for (final uwf in wrappedList) { + final userID = uwf.userInfo.userID; + if (userID != null) { + // 查找对应关系 + final match = shipResData.firstWhere( + (e) => e.userID == userID, + orElse: () => V2TimFollowTypeCheckResult(userID: ''), + ); + if (match.userID?.isNotEmpty ?? false) { + uwf.followType = match.followType ?? 0; + } + } + } + } + } + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(wrappedList); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '关注', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.userInfo.userID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 昵称 + 描述) + Expanded( + child: InkWell( + onTap: () { + Get.toNamed( + '/vloger', + arguments: item.userInfo.userID, + ); + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.userInfo.faceUrl, + width: 50, + height: 50, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.userInfo.selfSignature!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + SizedBox(width: 10), + + // 右侧按钮 + TextButton( + style: TextButton.styleFrom( + backgroundColor: item.followType == 3 ? Colors.grey : FStyle.primaryColor, + minimumSize: const Size(70, 32), + padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () async { + final ctl = Get.find(); + final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); + int realFollowType = 0; + if (checkRes.success && checkRes.data != null) { + realFollowType = checkRes.data!.first.followType ?? 0; + if ([1, 3].contains(realFollowType)) { + // 取关 + final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); + if (unRes.success) { + setState(() { + item.followType = 2; + }); + ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); + } + } else { + // 关注 + final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (res.success) { + setState(() { + item.followType = realFollowType == 0 + ? 1 + : realFollowType == 2 + ? 3 + : 0; + }); + final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (chatRes.success) { + final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } + } + } + }, + child: Text( + Utils.getTipText(item.followType), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/groupChat/groupDetail.dart b/lib/pages/groupChat/groupDetail.dart index c4d8183..1410725 100644 --- a/lib/pages/groupChat/groupDetail.dart +++ b/lib/pages/groupChat/groupDetail.dart @@ -44,7 +44,7 @@ class GroupdetailState extends State { void pickFaceUrl(BuildContext context) async { final hasPer = await Permissions.requestPhotoPermission(); if (!hasPer) { - Permissions.showPermissionDialog(); + Permissions.showPermissionDialog('相册'); return; } final pickedAssets = await AssetPicker.pickAssets( diff --git a/lib/pages/index/index.dart b/lib/pages/index/index.dart index 0052bb3..041af63 100644 --- a/lib/pages/index/index.dart +++ b/lib/pages/index/index.dart @@ -117,7 +117,8 @@ class _IndexPageState extends State with SingleTickerProviderStateMix void initState() { super.initState(); controller = Get.find(); - controller.initTabs(vsync: this); + // controller.initTabs(vsync: this); + controller.initTabs(); } @override @@ -129,7 +130,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix _buildTopSection(), // 内容区域 Expanded( - child: controller.tabController == null + child: controller.tabList.isEmpty ? Center(child: CircularProgressIndicator()) : Obx( () { @@ -153,7 +154,9 @@ class _IndexPageState extends State with SingleTickerProviderStateMix }).toList(); return DynamicTabBarWidget( onTabControllerUpdated: (tabController) { - controller.tabController = tabController; + // controller.tabController = tabController; + // controller.initTabs(changeController: tabController, vsync: this); + controller.addTabListener(tabController); // 强制选中第一个 if (tabController.index != 0) { tabController.animateTo(0); @@ -205,7 +208,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix // 构建顶部固定区域 Widget _buildTopSection() { - if (controller.tabController == null) { + if (controller.swiperData.isEmpty) { return SizedBox(); } return Column( diff --git a/lib/pages/index/indexcopy.dart b/lib/pages/index/indexcopy.dart index 2d51ea7..af4815c 100644 --- a/lib/pages/index/indexcopy.dart +++ b/lib/pages/index/indexcopy.dart @@ -116,7 +116,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix @override void initState() { super.initState(); - controller.initTabs(vsync: this); + // controller.initTabs(vsync: this); } @override diff --git a/lib/pages/my/user_info.dart b/lib/pages/my/user_info.dart index b684347..4420f27 100644 --- a/lib/pages/my/user_info.dart +++ b/lib/pages/my/user_info.dart @@ -1,13 +1,17 @@ +import 'dart:io'; + import 'package:bottom_picker/bottom_picker.dart'; import 'package:city_pickers/city_pickers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:image_cropper/image_cropper.dart'; import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/api/common_api.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/image_utils.dart'; import 'package:loopin/utils/index.dart'; import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; @@ -208,7 +212,7 @@ class _UserInfoState extends State { void pickCover(BuildContext context) async { final hasPer = await Permissions.requestPhotoPermission(); if (!hasPer) { - Permissions.showPermissionDialog(); + Permissions.showPermissionDialog('相册'); return; } final pickedAssets = await AssetPicker.pickAssets( @@ -252,7 +256,7 @@ class _UserInfoState extends State { void pickFaceUrl(BuildContext context) async { final hasPer = await Permissions.requestPhotoPermission(); if (!hasPer) { - Permissions.showPermissionDialog(); + Permissions.showPermissionDialog('相册'); return; } final pickedAssets = await AssetPicker.pickAssets( @@ -281,12 +285,46 @@ class _UserInfoState extends State { } else { print("图片合法,大小:$sizeInMB MB"); //走upload(file)上传图片拿到url地址 - final istance = MyDialog.loading('上传中'); - final res = await Http.upload(CommonApi.uploadFile, filePath: file.path); - userInfoController.faceUrl.value = res['data']['url']; - userInfoController.updateFaceUrl(); - userInfoController.customInfo.refresh(); - istance.close(); + + final croppedFile = await ImageCropper().cropImage( + sourcePath: file.path, + maxWidth: 1024, + maxHeight: null, + compressFormat: ImageCompressFormat.png, + compressQuality: 100, // png 时无效 + uiSettings: [ + AndroidUiSettings( + toolbarTitle: '裁剪', + toolbarColor: Colors.black, + toolbarWidgetColor: Colors.white, + hideBottomControls: false, + lockAspectRatio: false, + cropStyle: CropStyle.circle, + ), + IOSUiSettings( + title: '裁剪', + doneButtonTitle: '确认', + cropStyle: CropStyle.circle, + aspectRatioPickerButtonHidden: true, // 隐藏比例选择按钮 + resetAspectRatioEnabled: false, // 点击“重置”按钮时将裁剪框恢复到原始图片比例 + cancelButtonTitle: '返回', + aspectRatioLockEnabled: false, // 锁定裁剪框比例 + rotateButtonsHidden: false, + resetButtonHidden: false, + ), + ], + ); + //--- + if (croppedFile != null) { + final istance = MyDialog.loading('上传中'); + // 处理 + final uploadImg = await ImageUtils.toCircleImageFile(imageFile: File(croppedFile.path)); + final res = await Http.upload(CommonApi.uploadFile, filePath: uploadImg); + userInfoController.faceUrl.value = res['data']['url']; + userInfoController.updateFaceUrl(); + userInfoController.customInfo.refresh(); + istance.close(); + } } } } diff --git a/lib/pages/upload_video_page/upload_video_page.dart b/lib/pages/upload_video_page/upload_video_page.dart index 1180da7..835cdde 100644 --- a/lib/pages/upload_video_page/upload_video_page.dart +++ b/lib/pages/upload_video_page/upload_video_page.dart @@ -59,7 +59,7 @@ class _UploadVideoPageState extends State { final hasPer = await Permissions.requestVideoPermission(); if (!hasPer) { - Permissions.showPermissionDialog(); + Permissions.showPermissionDialog('相册'); return; } @@ -108,7 +108,7 @@ class _UploadVideoPageState extends State { final hasPer = await Permissions.requestPhotoPermission(); if (!hasPer) { - Permissions.showPermissionDialog(); + Permissions.showPermissionDialog('相册'); return; } diff --git a/lib/pages/video/commonVideo.dart b/lib/pages/video/commonVideo.dart index ad2337c..b16df86 100644 --- a/lib/pages/video/commonVideo.dart +++ b/lib/pages/video/commonVideo.dart @@ -516,7 +516,15 @@ class _VideoDetailPageState extends State { data: makeJson, ); if (res.success) { - final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + // final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + final isGroup = conv.groupID != null && conv.groupID!.isNotEmpty; + final sendRes = await IMMessage().sendMessage( + msg: res.data!.messageInfo!, + groupID: isGroup ? conv.groupID : '', + toUserID: isGroup ? '' : conv.userID, + cloudCustomData: SummaryType.shareVideo, + groupName: isGroup ? conv.showName : '', + ); if (sendRes.success) { MyToast().tip( title: '分享成功', diff --git a/lib/pages/video/module/attention.dart b/lib/pages/video/module/attention.dart index c3422e3..6bebdc6 100644 --- a/lib/pages/video/module/attention.dart +++ b/lib/pages/video/module/attention.dart @@ -15,12 +15,12 @@ import 'package:loopin/IM/im_service.dart' hide logger; import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/download_video.dart'; import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; -import 'package:loopin/models/share_type.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -997,7 +997,15 @@ class _AttentionModuleState extends State { data: makeJson, ); if (res.success) { - final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + // final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + final isGroup = conv.groupID != null && conv.groupID!.isNotEmpty; + final sendRes = await IMMessage().sendMessage( + msg: res.data!.messageInfo!, + groupID: isGroup ? conv.groupID : '', + toUserID: isGroup ? '' : conv.userID, + cloudCustomData: SummaryType.shareVideo, + groupName: isGroup ? conv.showName : '', + ); if (sendRes.success) { MyToast().tip( title: '分享成功', @@ -1019,398 +1027,399 @@ class _AttentionModuleState extends State { color: Colors.black, child: Column( children: [ - // 添加暂无数据提示 - if (videoList.isEmpty && !isLoadingMore) - Expanded( - child: Center( - child: Text( - '暂无数据', - style: TextStyle( - color: Colors.white, - fontSize: 16.0, + // 添加暂无数据提示 + if (videoList.isEmpty && !isLoadingMore) + Expanded( + child: Center( + child: Text( + '暂无数据', + style: TextStyle( + color: Colors.white, + fontSize: 16.0, + ), ), ), - ), - ) + ) else - Expanded( - child: Stack( - children: [ - /// 垂直滚动模块 - PageView.builder( - scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), - scrollDirection: Axis.vertical, - controller: pageController, - onPageChanged: (index) async { - videoModuleController.updateVideoPlayIndex(index); - setState(() { - sliderValue = 0.0; - sliderDraging = false; - position = Duration.zero; - duration = Duration.zero; - }); + Expanded( + child: Stack( + children: [ + /// 垂直滚动模块 + PageView.builder( + scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), + scrollDirection: Axis.vertical, + controller: pageController, + onPageChanged: (index) async { + videoModuleController.updateVideoPlayIndex(index); + setState(() { + sliderValue = 0.0; + sliderDraging = false; + position = Duration.zero; + duration = Duration.zero; + }); - player.stop(); - await player.open(Media(videoList[index]['url'])); - if (index == videoList.length - 2 && !isLoadingMore) { - await fetchVideoList(); - } - }, - itemCount: videoList.length, - itemBuilder: (context, index) { - final videoWidth = videoList[index]['width'] ?? 1; - final videoHeight = videoList[index]['height'] ?? 1; - final isHorizontal = videoWidth > videoHeight; - final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); - return Stack( - children: [ - // 视频区域 - Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: GestureDetector( - child: Stack( - children: [ - Visibility( - visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, - child: Video( - controller: videoController, - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - controls: NoVideoControls, - ), - ), - AnimatedOpacity( - opacity: videoModuleController.videoPlayIndex.value == index && position > Duration(milliseconds: 100) ? 0.0 : 1.0, - duration: Duration(milliseconds: 50), - child: Image.network( - videoList[index]['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - width: double.infinity, - height: double.infinity, - ), - ), - StreamBuilder( - stream: player.stream.playing, - builder: (context, playing) { - return Visibility( - visible: playing.data == false, - child: Center( - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () { - player.playOrPause(); - }, - icon: Icon( - playing.data == true ? Icons.pause : Icons.play_arrow_rounded, - color: Colors.white60, - size: 80, - ), - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), - ), - ), - ); - }, - ), - ], - ), - onTap: () { - player.playOrPause(); - }, - ), - ), - // 右侧操作栏 - Positioned( - bottom: 100.0, - right: 6.0, - child: Column( - spacing: 15.0, - children: [ - Stack( + player.stop(); + await player.open(Media(videoList[index]['url'])); + if (index == videoList.length - 2 && !isLoadingMore) { + await fetchVideoList(); + } + }, + itemCount: videoList.length, + itemBuilder: (context, index) { + final videoWidth = videoList[index]['width'] ?? 1; + final videoHeight = videoList[index]['height'] ?? 1; + final isHorizontal = videoWidth > videoHeight; + final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); + return Stack( + children: [ + // 视频区域 + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: GestureDetector( + child: Stack( children: [ - SizedBox( - height: 55.0, - width: 48.0, - child: GestureDetector( - onTap: () async { - player.pause(); - // 跳转到 Vloger 页面并等待返回结果 - final vloggerId = videoList[videoModuleController.videoPlayIndex.value]['memberId']; - final result = await Get.toNamed('/vloger', arguments: {'memberId':vloggerId}); - if (result != null) { - // 处理返回的参数 - print('返回的数据: ${result['followStatus']}'); - player.play(); - videoList[index]['doIFollowVloger'] = result['followStatus']; - } - }, - child: UnconstrainedBox( - alignment: Alignment.topCenter, - child: Container( - height: 48.0, - width: 48.0, - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 2.0), - borderRadius: BorderRadius.circular(100.0), - ), - child: ClipOval( - child: NetworkOrAssetImage( - imageUrl: videoList[index]['commentUserFace'], - ), - ), - ), - ), + Visibility( + visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, + child: Video( + controller: videoController, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + controls: NoVideoControls, ), ), - Positioned( - bottom: 0, - left: 15.0, - child: InkWell( - child: Container( - height: 18.0, - width: 18.0, - decoration: BoxDecoration( - color: videoList[index]['doIFollowVloger'] ? Colors.white : Color(0xFFFF5000), - borderRadius: BorderRadius.circular(100.0), - ), - child: Icon( - videoList[index]['doIFollowVloger'] ? Icons.check : Icons.add, - color: videoList[index]['doIFollowVloger'] ? Color(0xFFFF5000) : Colors.white, - size: 14.0, - ), - ), - onTap: () async { - final vlogerId = videoList[index]['memberId']; - final doIFollowVloger = videoList[index]['doIFollowVloger']; - // 未关注点击才去关注 - if (doIFollowVloger == false) { - final res = await ImService.instance.followUser(userIDList: [vlogerId]); - if (res.success) { - setState(() { - videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; - }); - } - } - }, + AnimatedOpacity( + opacity: videoModuleController.videoPlayIndex.value == index && position > Duration(milliseconds: 100) ? 0.0 : 1.0, + duration: Duration(milliseconds: 50), + child: Image.network( + videoList[index]['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + width: double.infinity, + height: double.infinity, ), ), + StreamBuilder( + stream: player.stream.playing, + builder: (context, playing) { + return Visibility( + visible: playing.data == false, + child: Center( + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + player.playOrPause(); + }, + icon: Icon( + playing.data == true ? Icons.pause : Icons.play_arrow_rounded, + color: Colors.white60, + size: 80, + ), + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), + ), + ), + ); + }, + ), ], ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/heart.svg', - colorFilter: ColorFilter.mode(videoList[index]['doILikeThisVlog'] ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoList[index]['likeCounts'] + (videoList[index]['doILikeThisVlog'] ? 1 : 0)}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], - ), - onTap: () { - logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); - if (videoList[index]['doILikeThisVlog'] == true) { - logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); - doUnLikeVideo(videoList[index]); - } else { - doLikeVideo(videoList[index]); - } - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/reply.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoList[index]['commentsCounts']}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], - ), - onTap: () { - handleComment(index); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/share.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: () { - handleShare(index); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/report.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: () async { - player.pause(); - // 跳转到举报页面并等待返回结果 - final result = await Get.toNamed('/report', arguments: videoList[videoModuleController.videoPlayIndex.value]); - if (result != null) { - player.play(); - } - }, - ), - ], - ), - ), - Positioned( - bottom: 15.0, - left: 10.0, - right: 80.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}', - style: const TextStyle(color: Colors.white, fontSize: 16.0), - ), - LayoutBuilder( - builder: (context, constraints) { - final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知'; - final span = TextSpan( - text: text, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ); - final tp = TextPainter( - text: span, - maxLines: 3, - textDirection: TextDirection.ltr, - ); - tp.layout(maxWidth: constraints.maxWidth); - final isOverflow = tp.didExceedMaxLines; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text, - maxLines: videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? null : 3, - overflow: - videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? TextOverflow.visible : TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ), - if (isOverflow) - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: GestureDetector( - onTap: () { - setState(() { - videoList[videoModuleController.videoPlayIndex.value]['expanded'] = - !videoList[videoModuleController.videoPlayIndex.value]['expanded']; - }); - }, - child: Text( - videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? '收起' : '展开更多', - textAlign: TextAlign.right, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ); - }, - ), - ], - )), - Positioned( - bottom: 0.0, - left: 6.0, - right: 6.0, - child: Visibility( - visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, - child: Listener( - child: SliderTheme( - data: SliderThemeData( - trackHeight: sliderDraging ? 6.0 : 2.0, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), - overlayShape: RoundSliderOverlayShape(overlayRadius: 0), - inactiveTrackColor: Colors.white24, - activeTrackColor: Colors.white, - thumbColor: Colors.white, - overlayColor: Colors.transparent, - ), - child: Slider( - value: sliderValue, - onChanged: (value) async { - setState(() { - sliderValue = value; - }); - await player.seek(duration * value.clamp(0.0, 1.0)); - }, - onChangeEnd: (value) async { - setState(() { - sliderDraging = false; - }); - if (!player.state.playing) { - await player.play(); - } - }, - ), - ), - onPointerMove: (e) { - setState(() { - sliderDraging = true; - }); + onTap: () { + player.playOrPause(); }, ), ), - ), - Positioned( - bottom: 100.0, - left: 10.0, - right: 10.0, - child: Visibility( - visible: sliderDraging, - child: DefaultTextStyle( - style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8.0, + // 右侧操作栏 + Positioned( + bottom: 100.0, + right: 6.0, + child: Column( + spacing: 15.0, + children: [ + Stack( children: [ - Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), - Text('/', style: TextStyle(fontSize: 14.0)), - Text(duration.label(reference: duration)), + SizedBox( + height: 55.0, + width: 48.0, + child: GestureDetector( + onTap: () async { + player.pause(); + // 跳转到 Vloger 页面并等待返回结果 + final vloggerId = videoList[videoModuleController.videoPlayIndex.value]['memberId']; + final result = await Get.toNamed('/vloger', arguments: {'memberId': vloggerId}); + if (result != null) { + // 处理返回的参数 + print('返回的数据: ${result['followStatus']}'); + player.play(); + videoList[index]['doIFollowVloger'] = result['followStatus']; + } + }, + child: UnconstrainedBox( + alignment: Alignment.topCenter, + child: Container( + height: 48.0, + width: 48.0, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2.0), + borderRadius: BorderRadius.circular(100.0), + ), + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: videoList[index]['commentUserFace'], + ), + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 15.0, + child: InkWell( + child: Container( + height: 18.0, + width: 18.0, + decoration: BoxDecoration( + color: videoList[index]['doIFollowVloger'] ? Colors.white : Color(0xFFFF5000), + borderRadius: BorderRadius.circular(100.0), + ), + child: Icon( + videoList[index]['doIFollowVloger'] ? Icons.check : Icons.add, + color: videoList[index]['doIFollowVloger'] ? Color(0xFFFF5000) : Colors.white, + size: 14.0, + ), + ), + onTap: () async { + final vlogerId = videoList[index]['memberId']; + final doIFollowVloger = videoList[index]['doIFollowVloger']; + // 未关注点击才去关注 + if (doIFollowVloger == false) { + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + if (res.success) { + setState(() { + videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; + }); + } + } + }, + ), + ), ], ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/heart.svg', + colorFilter: ColorFilter.mode(videoList[index]['doILikeThisVlog'] ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoList[index]['likeCounts'] + (videoList[index]['doILikeThisVlog'] ? 1 : 0)}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + if (videoList[index]['doILikeThisVlog'] == true) { + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + doUnLikeVideo(videoList[index]); + } else { + doLikeVideo(videoList[index]); + } + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/reply.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoList[index]['commentsCounts']}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + handleComment(index); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/share.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () { + handleShare(index); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/report.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () async { + player.pause(); + // 跳转到举报页面并等待返回结果 + final result = await Get.toNamed('/report', arguments: videoList[videoModuleController.videoPlayIndex.value]); + if (result != null) { + player.play(); + } + }, + ), + ], + ), + ), + Positioned( + bottom: 15.0, + left: 10.0, + right: 80.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}', + style: const TextStyle(color: Colors.white, fontSize: 16.0), + ), + LayoutBuilder( + builder: (context, constraints) { + final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知'; + final span = TextSpan( + text: text, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ); + final tp = TextPainter( + text: span, + maxLines: 3, + textDirection: TextDirection.ltr, + ); + tp.layout(maxWidth: constraints.maxWidth); + final isOverflow = tp.didExceedMaxLines; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + maxLines: videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? null : 3, + overflow: videoList[videoModuleController.videoPlayIndex.value]['expanded'] + ? TextOverflow.visible + : TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ), + if (isOverflow) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: GestureDetector( + onTap: () { + setState(() { + videoList[videoModuleController.videoPlayIndex.value]['expanded'] = + !videoList[videoModuleController.videoPlayIndex.value]['expanded']; + }); + }, + child: Text( + videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? '收起' : '展开更多', + textAlign: TextAlign.right, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + }, + ), + ], )), - ), - ], - ); - }, - ), - ], + Positioned( + bottom: 0.0, + left: 6.0, + right: 6.0, + child: Visibility( + visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, + child: Listener( + child: SliderTheme( + data: SliderThemeData( + trackHeight: sliderDraging ? 6.0 : 2.0, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), + overlayShape: RoundSliderOverlayShape(overlayRadius: 0), + inactiveTrackColor: Colors.white24, + activeTrackColor: Colors.white, + thumbColor: Colors.white, + overlayColor: Colors.transparent, + ), + child: Slider( + value: sliderValue, + onChanged: (value) async { + setState(() { + sliderValue = value; + }); + await player.seek(duration * value.clamp(0.0, 1.0)); + }, + onChangeEnd: (value) async { + setState(() { + sliderDraging = false; + }); + if (!player.state.playing) { + await player.play(); + } + }, + ), + ), + onPointerMove: (e) { + setState(() { + sliderDraging = true; + }); + }, + ), + ), + ), + Positioned( + bottom: 100.0, + left: 10.0, + right: 10.0, + child: Visibility( + visible: sliderDraging, + child: DefaultTextStyle( + style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8.0, + children: [ + Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), + Text('/', style: TextStyle(fontSize: 14.0)), + Text(duration.label(reference: duration)), + ], + ), + )), + ), + ], + ); + }, + ), + ], + ), ), - ), ], ), ); diff --git a/lib/pages/video/module/friend.dart b/lib/pages/video/module/friend.dart index 7936fec..6e6cafe 100644 --- a/lib/pages/video/module/friend.dart +++ b/lib/pages/video/module/friend.dart @@ -1002,7 +1002,15 @@ class _FriendModuleState extends State { data: makeJson, ); if (res.success) { - final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + // final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + final isGroup = conv.groupID != null && conv.groupID!.isNotEmpty; + final sendRes = await IMMessage().sendMessage( + msg: res.data!.messageInfo!, + groupID: isGroup ? conv.groupID : '', + toUserID: isGroup ? '' : conv.userID, + cloudCustomData: SummaryType.shareVideo, + groupName: isGroup ? conv.showName : '', + ); if (sendRes.success) { MyToast().tip( title: '分享成功', @@ -1141,8 +1149,8 @@ class _FriendModuleState extends State { onTap: () async { player.pause(); // 跳转到 Vloger 页面并等待返回结果 - final vloggerId = videoList[videoModuleController.videoPlayIndex.value]['memberId']; - final result = await Get.toNamed('/vloger', arguments: {'memberId':vloggerId}); + final vloggerId = videoList[videoModuleController.videoPlayIndex.value]['memberId']; + final result = await Get.toNamed('/vloger', arguments: {'memberId': vloggerId}); if (result != null) { // 处理返回的参数 print('返回的数据: ${result['followStatus']}'); diff --git a/lib/pages/video/module/recommend.dart b/lib/pages/video/module/recommend.dart index 8d43c3a..8dc36c8 100644 --- a/lib/pages/video/module/recommend.dart +++ b/lib/pages/video/module/recommend.dart @@ -981,7 +981,6 @@ class _RecommendModuleState extends State { void handlCoverClick(V2TimConversation conv) async { // 发送VideoMsg,获取当前视频信息 - final userId = conv.userID; final currentVideo = videoList[videoModuleController.videoPlayIndex.value]; logger.w(currentVideo); final img = (currentVideo['cover'] != null && currentVideo['cover'].toString().isNotEmpty) ? currentVideo['cover'] : currentVideo['firstFrameImg']; @@ -1001,7 +1000,16 @@ class _RecommendModuleState extends State { data: makeJson, ); if (res.success) { - final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); + final isGroup = conv.groupID != null && conv.groupID!.isNotEmpty; + logger.w(isGroup); + logger.w(conv.toJson()); + final sendRes = await IMMessage().sendMessage( + msg: res.data!.messageInfo!, + groupID: isGroup ? conv.groupID : '', + toUserID: isGroup ? '' : conv.userID, + cloudCustomData: SummaryType.shareVideo, + groupName: isGroup ? conv.showName : '', + ); if (sendRes.success) { MyToast().tip( title: '分享成功', @@ -1127,7 +1135,7 @@ class _RecommendModuleState extends State { player.pause(); // 跳转到 Vloger 页面并等待返回结果 final vloggerId = videoList[videoModuleController.videoPlayIndex.value]['memberId']; - final result = await Get.toNamed('/vloger', arguments: {'memberId':vloggerId}); + final result = await Get.toNamed('/vloger', arguments: {'memberId': vloggerId}); if (result != null) { // 处理返回的参数 print('返回的数据: ${result['followStatus']}'); diff --git a/lib/router/index.dart b/lib/router/index.dart index 38a92bc..f59f508 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -8,6 +8,7 @@ import 'package:loopin/pages/chat/chat.dart'; import 'package:loopin/pages/chat/chat_group.dart'; import 'package:loopin/pages/chat/chat_no_friend.dart'; import 'package:loopin/pages/chat/notify/interaction.dart'; +import 'package:loopin/pages/chat/notify/newFoucs.dart'; import 'package:loopin/pages/chat/notify/noFriend.dart'; import 'package:loopin/pages/chat/notify/system.dart'; import 'package:loopin/pages/groupChat/groupList.dart'; @@ -69,6 +70,7 @@ final Map routes = { '/nickName': const NickName(), //通知相关 '/noFriend': const Nofriend(), + '/newFocus': const Newfoucs(), '/system': const System(), '/interaction': const Interaction(), //关系链 diff --git a/lib/styles/index.dart b/lib/styles/index.dart index 2f8bd3c..d45ab57 100644 --- a/lib/styles/index.dart +++ b/lib/styles/index.dart @@ -32,11 +32,14 @@ class FStyle { // 颜色 // static const backgroundColor = Color(0xFFEEEEEE); - static const backgroundColor = Colors.white; + static const backgroundColor = Color(0xFFFDF6F0); static const primaryColor = Color(0xFFFF5000); static const white = Colors.white; static const c999 = Color(0xFF999999); - + static const secondaryColor = Color(0xFFFFC18E); + static const inputBackground = Color(0xFFFFF2ED); + static const textPrimary = Color(0xFF2E2E2E); + static const textSecondary = Color(0xFF6F6F6F); // 间距 static mt(double v) => EdgeInsets.only(top: v); static mb(double v) => EdgeInsets.only(bottom: v); diff --git a/lib/update/upgrade_service.dart b/lib/update/upgrade_service.dart index 2afa19b..115ec6c 100644 --- a/lib/update/upgrade_service.dart +++ b/lib/update/upgrade_service.dart @@ -22,8 +22,9 @@ class UpgradeService { }); if (!state.mounted) return; - // logger.i(res); + logger.w(res); final result = res['data']['records'] as List; + if (result.isEmpty) return; final data = result.first; final currentVersion = info.buildNumber; if (currentVersion != data['versionCode']) { diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart new file mode 100644 index 0000000..6b0ed05 --- /dev/null +++ b/lib/utils/image_utils.dart @@ -0,0 +1,72 @@ +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class ImageUtils { + /// 将图片裁剪成圆形,返回 PNG 文件路径 + /// [imageFile] 或 [imageData] 二选一 + static Future toCircleImageFile({ + Uint8List? imageData, + File? imageFile, + }) async { + if (imageData == null && imageFile == null) { + throw ArgumentError('必须提供 imageData 或 imageFile'); + } + + // 读取原始字节 + final bytes = imageData ?? await imageFile!.readAsBytes(); + + // 1. 解码图片 + final codec = await ui.instantiateImageCodec(bytes); + final frame = await codec.getNextFrame(); + final original = frame.image; + + final width = original.width; + final height = original.height; + final size = math.min(width, height); + + // 2. 画布 + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + final paint = Paint()..isAntiAlias = true; + + // 3. 圆形遮罩 + final center = Offset(size / 2, size / 2); + canvas.drawCircle(center, size / 2, paint..color = Colors.white); + + // 4. 设置混合模式为 srcIn + paint.blendMode = BlendMode.srcIn; + + // 5. 绘制原图(取正方形区域,保证圆形不变形) + final srcRect = Rect.fromLTWH( + (width - size) / 2, + (height - size) / 2, + size.toDouble(), + size.toDouble(), + ); + final dstRect = Rect.fromLTWH(0, 0, size.toDouble(), size.toDouble()); + + canvas.drawImageRect(original, srcRect, dstRect, paint); + + // 6. 导出为 Image + final picture = recorder.endRecording(); + final imgFinal = await picture.toImage(size, size); + + // 7. 转 PNG 字节 + final byteData = await imgFinal.toByteData(format: ui.ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + // 8. 存临时文件 + final tempDir = await getTemporaryDirectory(); + final file = File( + '${tempDir.path}/circle_${DateTime.now().millisecondsSinceEpoch}.png', + ); + await file.writeAsBytes(pngBytes); + + return file.path; + } +} diff --git a/lib/utils/notification_banner.dart b/lib/utils/notification_banner.dart index f4fa00c..28af460 100644 --- a/lib/utils/notification_banner.dart +++ b/lib/utils/notification_banner.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; import 'package:loopin/utils/parse_message_summary.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; @@ -27,21 +28,48 @@ class NotificationBanner { } final text = parseMessageSummary(msg); Get.snackbar( - name, - text, - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, + '', + '', + duration: const Duration(minutes: 1), margin: const EdgeInsets.all(12), - backgroundColor: Get.theme.cardColor, - colorText: Get.theme.textTheme.bodyLarge?.color, - icon: ClipOval( - child: NetworkOrAssetImage( - imageUrl: avatar, - placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png', + backgroundColor: FStyle.primaryColor.withAlpha(220), + titleText: Row( + children: [ + // 头像 + ClipOval( + child: NetworkOrAssetImage( + imageUrl: avatar, + placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png', + width: 50, + height: 50, + ), + ), + const SizedBox(width: 8), + // 文本 + Expanded( + child: Text( + name, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + messageText: Text( + text, + style: const TextStyle( + color: Colors.white, + fontSize: 14, ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ), onTap: (_) async { - // 点击后立刻关闭 Get.closeCurrentSnackbar(); String? conversationID; if (msg.groupID != null && msg.groupID!.isNotEmpty) { @@ -52,17 +80,56 @@ class NotificationBanner { final cRes = await ImService.instance.getConversation(conversationID: conversationID!); if (cRes.success) { if (msg.userID != null) { - // 单聊消息 Get.toNamed('/chat', arguments: cRes.data); } else if (msg.groupID != null) { Get.toNamed('/chatGroup', arguments: cRes.data); } } else { - // 异常 - MyDialog.toast(cRes.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + MyDialog.toast( + cRes.desc, + icon: const Icon(Icons.warning), + style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)), + ); } }, ); + + // Get.snackbar( + // name, + // text, + // duration: Duration(minutes: 1), + // margin: const EdgeInsets.all(12), + // backgroundColor: FStyle.primaryColor, + // colorText: Colors.white, + // icon: ClipOval( + // child: NetworkOrAssetImage( + // imageUrl: avatar, + // placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png', + // ), + // ), + // onTap: (_) async { + // // 点击后立刻关闭 + // Get.closeCurrentSnackbar(); + // String? conversationID; + // if (msg.groupID != null && msg.groupID!.isNotEmpty) { + // conversationID = 'group_${msg.groupID}'; + // } else if (msg.userID != null && msg.userID!.isNotEmpty) { + // conversationID = 'c2c_${msg.userID}'; + // } + // final cRes = await ImService.instance.getConversation(conversationID: conversationID!); + // if (cRes.success) { + // if (msg.userID != null) { + // // 单聊消息 + // Get.toNamed('/chat', arguments: cRes.data); + // } else if (msg.groupID != null) { + // Get.toNamed('/chatGroup', arguments: cRes.data); + // } + // } else { + // // 异常 + // MyDialog.toast(cRes.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + // } + // }, + // ); } /// 被关注通知 diff --git a/lib/utils/permissions.dart b/lib/utils/permissions.dart index 1110bc8..63f4ffe 100644 --- a/lib/utils/permissions.dart +++ b/lib/utils/permissions.dart @@ -25,19 +25,14 @@ class Permissions { } } else if (Platform.isIOS) { final status = await Permission.photos.request(); - return status.isGranted; + return status.isGranted || status.isLimited; + } else { + return false; } - return false; - } - - // 请求相机权限 - static Future requestCameraPermission() async { - return await _checkAndRequest(Permission.camera); } // 请求相册权限 static Future requestPhotoPermission() async { - // return await _checkAndRequest(Permission.photos); if (Platform.isAndroid) { final deviceInfoPlugin = DeviceInfoPlugin(); final androidInfo = await deviceInfoPlugin.androidInfo; @@ -46,17 +41,27 @@ class Permissions { if (sdkInt >= 33) { // Android 13 及以上 final status = await Permission.photos.request(); - return status.isGranted; + // return status.isGranted; + return handleStatus(status, isAndroid: true); } else { // Android 12 及以下 final status = await Permission.storage.request(); - return status.isGranted; + // return status.isGranted; + return handleStatus(status, isAndroid: true); } } else if (Platform.isIOS) { final status = await Permission.photos.request(); - return status.isGranted; + logger.w('iOS photos = $status'); + // return status.isGranted || status.isLimited; + return handleStatus(status, isAndroid: false); + } else { + return false; } - return false; + } + + // 请求相机权限 + static Future requestCameraPermission() async { + return await _checkAndRequest(Permission.camera); } // 请求麦克风权限 @@ -81,7 +86,7 @@ class Permissions { if (result.isPermanentlyDenied) { // 永久拒绝 只能去设置 - showPermissionDialog(); + return false; } else { // 临时拒绝 提示 Get.snackbar('权限请求失败', '无法访问,请授权对应权限后重试'); @@ -90,10 +95,45 @@ class Permissions { return false; } + /// 处理权限状态 + static bool handleStatus(PermissionStatus status, {required bool isAndroid}) { + logger.w("当前权限状态 = $status"); + logger.e(status.isPermanentlyDenied); + if (status.isGranted) { + return true; + } + + if (!isAndroid) { + // iOS 独有的情况 + if (status.isLimited) { + logger.w("iOS 相册权限 = 仅限部分照片 (Limited)"); + return true; // Limited 状态下也能用,但受限制 + } + if (status.isPermanentlyDenied) { + // debug 模式或已授权的真机,有时误判为 permanentlyDenied + // 直接尝试访问,或者返回 true + logger.w("可能已授权,直接允许访问"); + return true; + } + } + + if (status.isDenied) { + logger.w("相册权限被拒绝(可再次请求)"); + return false; + } + + if (status.isPermanentlyDenied || status.isRestricted) { + logger.w("相册权限被永久拒绝,需要跳转到设置页"); + return false; + } + + return false; + } + // 跳转设置的提示弹窗 - static void showPermissionDialog() async { + static void showPermissionDialog(String name) async { final confirmed = await ConfirmDialog.show( - title: '需要权限', + title: '需要$name权限', content: '请前往系统设置中手动开启权限', confirmText: '去设置', ); diff --git a/lib/utils/scan_code_type.dart b/lib/utils/scan_code_type.dart index 2453655..bd3c72e 100644 --- a/lib/utils/scan_code_type.dart +++ b/lib/utils/scan_code_type.dart @@ -1,8 +1,14 @@ // 扫码 类型 +class QrTypeCode { + static const String hxm = 'hxm-'; + static const String hym = 'hym-'; + static const String tgm = 'tgm-'; +} + enum ScanCodeType { verification('hxm'), // 核销码 - friend('hym'), // 好友码 - promotion('tgm'); // 推广码 + friend('hym'), // 好友码 + promotion('tgm'); // 推广码 final String prefix; const ScanCodeType(this.prefix); @@ -26,4 +32,4 @@ enum ScanCodeType { } return null; } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index ff06fce..3f5e7f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" card_swiper: dependency: "direct main" description: @@ -350,6 +374,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" flutter_form_builder: dependency: "direct main" description: @@ -451,6 +483,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.28" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: e6bd17290cf0d011f9ed66c74d4159b8fe3b3050afedac0f11fab1ba8687e710 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" flutter_staggered_grid_view: dependency: "direct main" description: @@ -605,6 +645,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.5.4" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.0" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.1.0" + image_gallery_saver_plus: + dependency: "direct main" + description: + name: image_gallery_saver_plus + sha256: "199b9e24f8d85e98f11e3d35571ab68ae50626ad40e2bb85c84383f69a6950ad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" image_picker: dependency: "direct main" description: @@ -893,6 +965,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -1085,6 +1165,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.0.2" + pretty_qr_code: + dependency: "direct main" + description: + name: pretty_qr_code + sha256: "2291db3f68d70a3dcd46c6bd599f30991ae4c02f27f36215fbb3f4865a609259" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.0" provider: dependency: transitive description: @@ -1173,6 +1261,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.6" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.28.0" safe_local_storage: dependency: transitive description: @@ -1234,6 +1330,46 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bee201f..f80a46a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,7 +67,8 @@ dependencies: flutter_upgrader: ^1.1.20 #更新 path_provider: ^2.1.2 - permission_handler: ^12.0.0+1 + permission_handler: ^12.0.0+1 #权限 + image_gallery_saver_plus: ^4.0.1 #存储 install_plugin: ^2.1.0 # Android 安装 APK flutter_native_splash: ^2.4.6 # 启动图 @@ -94,6 +95,10 @@ dependencies: image_picker: ^1.2.0 #相机 video_player: ^2.10.0 #视频处理 mime: ^2.0.0 #文件类型推断 + flutter_slidable: ^4.0.1 + cached_network_image: ^3.4.1 + image_cropper: ^9.1.0 + pretty_qr_code: ^3.5.0 dev_dependencies: flutter_launcher_icons: ^0.13.1 # 使用最新版本