From 41671f9e43034d30695342ab973f9f61393e82f2 Mon Sep 17 00:00:00 2001 From: NIGGER SLAYER Date: Tue, 14 Apr 2026 17:21:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E7=94=BB=20=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E6=A1=86=E7=AD=89=E5=B7=B2=E7=9F=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/gift/gift_page.dart | 112 +- lib/modules/gift/gift_tab_page.dart | 21 +- .../popular/follow/sc_room_follow_page.dart | 6 +- .../popular/history/sc_room_history_page.dart | 10 +- .../home/popular/mine/sc_home_mine_page.dart | 26 +- .../popular/party/sc_home_party_page.dart | 45 +- lib/modules/room/detail/room_detail_page.dart | 934 ++++++------ lib/modules/room/edit/room_edit_page.dart | 1109 ++++++++------- lib/modules/room/voice_room_page.dart | 10 +- lib/modules/search/sc_search_page.dart | 3 +- .../user/profile/person_detail_page.dart | 1265 +++++++++-------- lib/services/audio/rtc_manager.dart | 65 +- lib/services/audio/rtm_manager.dart | 244 ++-- .../general/sc_app_general_manager.dart | 530 ++++--- lib/services/gift/gift_animation_manager.dart | 6 +- lib/services/room/rc_room_manager.dart | 2 +- .../business_logic/models/res/login_res.dart | 4 +- .../models/res/sc_edit_room_info_res.dart | 2 +- .../models/enum/sc_gift_type.dart | 23 +- .../sc_general_repository_imp.dart | 45 +- .../repositories/sc_room_repository_imp.dart | 43 +- .../repositories/sc_user_repository_impl.dart | 50 +- .../tools/sc_gift_vap_svga_manager.dart | 872 ++++++++---- lib/shared/tools/sc_path_utils.dart | 3 +- lib/shared/tools/sc_room_profile_cache.dart | 146 ++ lib/ui_kit/components/sc_compontent.dart | 150 +- lib/ui_kit/components/sc_float_ichart.dart | 410 +++--- .../room/effect/vapp_svga_layer_widget.dart | 51 +- .../floating/floating_gift_screen_widget.dart | 14 +- .../floating_luck_gift_screen_widget.dart | 8 +- lib/ui_kit/widgets/room/room_head_widget.dart | 20 +- .../widgets/room/room_online_user_widget.dart | 349 ++--- sc_images/general/sc_no_data.png | Bin 0 -> 41895 bytes 需求进度.md | 46 + 34 files changed, 3762 insertions(+), 2862 deletions(-) create mode 100644 lib/shared/tools/sc_room_profile_cache.dart create mode 100644 sc_images/general/sc_no_data.png diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 7a3ec0e..1b895d3 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -1,9 +1,7 @@ import 'dart:convert'; import 'dart:ui' as ui; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -28,7 +26,6 @@ import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.d import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; -import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; @@ -81,6 +78,34 @@ class _GiftPageState extends State int giftType = 0; Debouncer debouncer = Debouncer(); + void _giftFxLog(String message) { + debugPrint('[GiftFX][Send] $message'); + } + + void _handleGiftSelected( + SocialChatGiftRes? gift, { + required int nextGiftType, + }) { + checkedGift = gift; + if (gift != null && + (gift.giftSourceUrl ?? "").isNotEmpty && + scGiftHasFullScreenEffect(gift.special)) { + SCGiftVapSvgaManager().preload(gift.giftSourceUrl!, highPriority: true); + _giftFxLog( + 'preload selected gift ' + 'giftId=${gift.id} ' + 'giftName=${gift.giftName} ' + 'giftSourceUrl=${gift.giftSourceUrl} ' + 'special=${gift.special}', + ); + } + number = 1; + noShowNumber = false; + setState(() { + giftType = nextGiftType; + }); + } + @override void initState() { super.initState(); @@ -91,20 +116,15 @@ class _GiftPageState extends State Provider.of(context, listen: false).balance(); _pages.add( GiftTabPage("ALL", (int checkedI) { - checkedGift = null; var all = Provider.of( context, listen: false, ).giftByTab["ALL"]; - if (all != null) { - checkedGift = all[checkedI]; - } - number = 1; - noShowNumber = false; - setState(() { - giftType = 0; - }); + _handleGiftSelected( + all != null ? all[checkedI] : null, + nextGiftType: 0, + ); }), ); @@ -426,7 +446,8 @@ class _GiftPageState extends State horizontal: 20.w, ), decoration: BoxDecoration( - color: SocialChatTheme.primaryLight, + color: + SocialChatTheme.primaryLight, borderRadius: BorderRadius.circular(5), ), @@ -743,6 +764,16 @@ class _GiftPageState extends State roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", ) .then((result) { + _giftFxLog( + 'giveGift success giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName} ' + 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' + 'special=${checkedGift?.special} ' + 'giftTab=${checkedGift?.giftTab} ' + 'number=$number ' + 'acceptUserIds=${acceptUserIds.join(",")} ' + 'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id}', + ); // SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful); sendGiftMsg(acceptUsers); Provider.of( @@ -750,12 +781,38 @@ class _GiftPageState extends State listen: false, ).updateBalance(result); }) - .catchError((e) {}); + .catchError((e) { + _giftFxLog( + 'giveGift failed giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName} ' + 'error=$e', + ); + }); } void sendGiftMsg(List acceptUsers) { ///发送一条IM消息 for (var u in acceptUsers) { + final special = checkedGift?.special ?? ""; + final giftSourceUrl = checkedGift?.giftSourceUrl ?? ""; + final hasSource = giftSourceUrl.isNotEmpty; + final hasAnimation = scGiftHasAnimationSpecial(special); + final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name); + final hasFullScreenEffect = scGiftHasFullScreenEffect(special); + _giftFxLog( + 'dispatch gift msg ' + 'giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName} ' + 'toUserId=${u.user?.id} ' + 'toUserName=${u.user?.userNickname} ' + 'giftSourceUrl=$giftSourceUrl ' + 'special=$special ' + 'hasSource=$hasSource ' + 'hasAnimation=$hasAnimation ' + 'hasGlobalGift=$hasGlobalGift ' + 'hasFullScreenEffect=$hasFullScreenEffect ' + 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', + ); Provider.of( navigatorKey.currentState!.context, listen: false, @@ -808,12 +865,35 @@ class _GiftPageState extends State OverlayManager().addMessage(fMsg); } if (checkedGift!.giftSourceUrl != null && checkedGift!.special != null) { - if (checkedGift!.special!.contains(SCGiftType.ANIMSCION.name) || - checkedGift!.special!.contains(SCGiftType.GLOBAL_GIFT.name)) { + if (scGiftHasFullScreenEffect(checkedGift!.special)) { if (SCGlobalConfig.isGiftSpecialEffects) { + _giftFxLog( + 'local trigger player play ' + 'path=${checkedGift!.giftSourceUrl} ' + 'giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName}', + ); SCGiftVapSvgaManager().play(checkedGift!.giftSourceUrl!); + } else { + _giftFxLog( + 'skip local play because isGiftSpecialEffects=false ' + 'giftId=${checkedGift?.id}', + ); } + } else { + _giftFxLog( + 'skip local play because special does not include ' + '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' + 'giftId=${checkedGift?.id} special=${checkedGift?.special}', + ); } + } else { + _giftFxLog( + 'skip local play because giftSourceUrl or special is null ' + 'giftId=${checkedGift?.id} ' + 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' + 'special=${checkedGift?.special}', + ); } } } diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index b179936..8d9af7d 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -56,7 +56,7 @@ class _GiftTabPageState extends State { ) : Column( children: [ - SizedBox(height: 23.w,), + SizedBox(height: 23.w), Expanded( child: PageView.builder( controller: _giftPageController, @@ -250,7 +250,7 @@ class _GiftTabPageState extends State { lineHeight: 1, textAlign: TextAlign.center, fontWeight: FontWeight.w600, - textColor: widget.isDark ? Colors.white : Colors.black, + textColor: widget.isDark ? Colors.white : Colors.black, ), ), Row( @@ -280,23 +280,29 @@ class _GiftTabPageState extends State { children: [ SizedBox(height: 3.w), // 只添加需要的 widget - if (gift.special!.contains(SCGiftType.ANIMSCION.name)) + if (scGiftHasFullScreenEffect(gift.special)) Image.asset( - _strategy.getGiftPageGiftEffectIcon(SCGiftType.ANIMSCION.name), + _strategy.getGiftPageGiftEffectIcon( + SCGiftType.ANIMSCION.name, + ), width: 16.w, height: 16.w, fit: BoxFit.fill, ), if (gift.special!.contains(SCGiftType.MUSIC.name)) Image.asset( - _strategy.getGiftPageGiftMusicIcon(SCGiftType.MUSIC.name), + _strategy.getGiftPageGiftMusicIcon( + SCGiftType.MUSIC.name, + ), width: 16.w, height: 16.w, fit: BoxFit.fill, ), if (gift.giftTab == (SCGiftType.LUCKY_GIFT.name)) Image.asset( - _strategy.getGiftPageGiftLuckIcon(SCGiftType.LUCKY_GIFT.name), + _strategy.getGiftPageGiftLuckIcon( + SCGiftType.LUCKY_GIFT.name, + ), width: 16.w, height: 16.w, fit: BoxFit.fill, @@ -333,7 +339,8 @@ class _GiftTabPageState extends State { margin: EdgeInsets.symmetric(horizontal: 4.w), decoration: BoxDecoration( shape: BoxShape.circle, - color: _index == i ? SocialChatTheme.primaryColor : Color(0xffDADADA), + color: + _index == i ? SocialChatTheme.primaryColor : Color(0xffDADADA), ), ), ); diff --git a/lib/modules/home/popular/follow/sc_room_follow_page.dart b/lib/modules/home/popular/follow/sc_room_follow_page.dart index 27a07c8..bbacb1c 100644 --- a/lib/modules/home/popular/follow/sc_room_follow_page.dart +++ b/lib/modules/home/popular/follow/sc_room_follow_page.dart @@ -63,7 +63,11 @@ class _RoomFollowPageState ), ), child: netImage( - url: roomRes.roomProfile?.roomCover ?? "", + url: resolveRoomCoverUrl( + roomRes.roomProfile?.id, + roomRes.roomProfile?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.circular(12.w), width: 200.w, height: 200.w, diff --git a/lib/modules/home/popular/history/sc_room_history_page.dart b/lib/modules/home/popular/history/sc_room_history_page.dart index 1afdaf3..abb29b6 100644 --- a/lib/modules/home/popular/history/sc_room_history_page.dart +++ b/lib/modules/home/popular/history/sc_room_history_page.dart @@ -63,7 +63,11 @@ class _SCRoomHistoryPageState ), ), child: netImage( - url: roomRes.roomProfile?.roomCover ?? "", + url: resolveRoomCoverUrl( + roomRes.roomProfile?.id, + roomRes.roomProfile?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.circular(12.w), width: 200.w, height: 200.w, @@ -74,7 +78,9 @@ class _SCRoomHistoryPageState margin: EdgeInsets.symmetric(horizontal: 1.w), decoration: BoxDecoration( image: DecorationImage( - image: AssetImage("sc_images/index/sc_icon_index_room_brd.png"), + image: AssetImage( + "sc_images/index/sc_icon_index_room_brd.png", + ), fit: BoxFit.fill, ), ), diff --git a/lib/modules/home/popular/mine/sc_home_mine_page.dart b/lib/modules/home/popular/mine/sc_home_mine_page.dart index a07b0e1..e0cf746 100644 --- a/lib/modules/home/popular/mine/sc_home_mine_page.dart +++ b/lib/modules/home/popular/mine/sc_home_mine_page.dart @@ -33,11 +33,7 @@ class _HomeMinePageState extends SCPageListState BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; late TabController _tabController; - final List _pages = [ - SCRoomHistoryPage(), - SCRoomFollowPage(), - - ]; + final List _pages = [SCRoomHistoryPage(), SCRoomFollowPage()]; bool isLoading = false; final List _tabs = []; @@ -124,7 +120,7 @@ class _HomeMinePageState extends SCPageListState physics: NeverScrollableScrollPhysics(), children: _pages, ), - ) + ), ], ); } @@ -137,7 +133,11 @@ class _HomeMinePageState extends SCPageListState children: [ SizedBox(width: 10.w), netImage( - url: provider.myRoom?.roomCover ?? "", + url: resolveRoomCoverUrl( + provider.myRoom?.id, + provider.myRoom?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.all(Radius.circular(8.w)), width: 70.w, ), @@ -323,7 +323,11 @@ class _HomeMinePageState extends SCPageListState child: Stack( children: [ netImage( - url: roomRes.roomProfile?.roomCover ?? "", + url: resolveRoomCoverUrl( + roomRes.roomProfile?.id, + roomRes.roomProfile?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, width: 70.w, height: 70.w, fit: BoxFit.cover, @@ -407,7 +411,11 @@ class _HomeMinePageState extends SCPageListState children: [ SizedBox(width: 20.w), netImage( - url: res.roomProfile?.roomCover ?? "", + url: resolveRoomCoverUrl( + res.roomProfile?.id, + res.roomProfile?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, fit: BoxFit.cover, borderRadius: BorderRadius.all(Radius.circular(12.w)), width: 85.w, diff --git a/lib/modules/home/popular/party/sc_home_party_page.dart b/lib/modules/home/popular/party/sc_home_party_page.dart index 06cb1c4..a94590e 100644 --- a/lib/modules/home/popular/party/sc_home_party_page.dart +++ b/lib/modules/home/popular/party/sc_home_party_page.dart @@ -427,7 +427,8 @@ class _HomePartyPageState extends State ), ), child: netImage( - url: res.roomCover ?? "", + url: resolveRoomCoverUrl(res.id, res.roomCover), + defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.circular(12.w), width: 200.w, height: 200.w, @@ -451,7 +452,7 @@ class _HomePartyPageState extends State builder: (_, provider, __) { return netImage( url: - "${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}", + "${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}", width: 20.w, height: 13.w, borderRadius: BorderRadius.circular(2.w), @@ -503,37 +504,25 @@ class _HomePartyPageState extends State ), ), SizedBox(width: 5.w), - (res.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? Image.asset( - "sc_images/general/sc_icon_online_user.png", - width: 14.w, - height: 14.w, - ) + "sc_images/general/sc_icon_online_user.png", + width: 14.w, + height: 14.w, + ) : Image.asset( - "sc_images/index/sc_icon_room_suo.png", - width: 20.w, - height: 20.w, - ), - (res.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + "sc_images/index/sc_icon_room_suo.png", + width: 20.w, + height: 20.w, + ), + (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? SizedBox(width: 3.w) : Container(height: 10.w), - (res.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? text( - res.extValues?.memberQuantity ?? "0", - fontSize: 12.sp, - ) + res.extValues?.memberQuantity ?? "0", + fontSize: 12.sp, + ) : Container(height: 10.w), SizedBox(width: 10.w), ], diff --git a/lib/modules/room/detail/room_detail_page.dart b/lib/modules/room/detail/room_detail_page.dart index e7b5487..6d2b505 100644 --- a/lib/modules/room/detail/room_detail_page.dart +++ b/lib/modules/room/detail/room_detail_page.dart @@ -97,13 +97,19 @@ class _RoomDetailPageState extends State { child: Row( children: [ netImage( - url: - ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomCover ?? - "", + url: resolveRoomCoverUrl( + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id, + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, width: 48.w, height: 48.w, borderRadius: BorderRadius.all( @@ -112,22 +118,24 @@ class _RoomDetailPageState extends State { ), SizedBox(width: 8.w), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Row( children: [ Container( constraints: BoxConstraints( maxWidth: - ScreenUtil().screenWidth * 0.6, + ScreenUtil().screenWidth * + 0.6, ), child: text( textColor: Colors.black, ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomName ?? + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomName ?? "", fontSize: 14.sp, ), @@ -135,10 +143,10 @@ class _RoomDetailPageState extends State { SizedBox(width: 3.w), widget.isHomeowner ? Image.asset( - "sc_images/room/sc_icon_room_edit.png", - width: 13.w, - height: 13.w, - ) + "sc_images/room/sc_icon_room_edit.png", + width: 13.w, + height: 13.w, + ) : Container(), ], ), @@ -165,11 +173,11 @@ class _RoomDetailPageState extends State { Clipboard.setData( ClipboardData( text: - ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomAccount ?? + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount ?? "", ), ); @@ -205,10 +213,13 @@ class _RoomDetailPageState extends State { color: Color(0xffF2F2F2), borderRadius: BorderRadius.circular(10.w), ), - margin: EdgeInsets.symmetric(horizontal: 15.w), + margin: EdgeInsets.symmetric( + horizontal: 15.w, + ), padding: EdgeInsets.all(15.w), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ text( "${SCAppLocalizations.of(context)!.roomOwner}:", @@ -220,11 +231,11 @@ class _RoomDetailPageState extends State { children: [ netImage( url: - ref - .currenRoom - ?.roomProfile - ?.userProfile - ?.userAvatar ?? + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.userAvatar ?? "", shape: BoxShape.circle, width: 40.w, @@ -233,36 +244,36 @@ class _RoomDetailPageState extends State { SizedBox(width: 10.w), Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ socialchatNickNameText( maxWidth: 135.w, textColor: Colors.black, ref - .currenRoom - ?.roomProfile - ?.userProfile - ?.userNickname ?? + .currenRoom + ?.roomProfile + ?.userProfile + ?.userNickname ?? "", fontSize: 14.sp, fontWeight: FontWeight.w600, type: - ref - .currenRoom - ?.roomProfile - ?.userProfile - ?.getVIP() - ?.name ?? + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.getVIP() + ?.name ?? "", needScroll: - (ref - .currenRoom - ?.roomProfile - ?.userProfile - ?.userNickname - ?.characters - .length ?? - 0) > + (ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > 13, ), Row( @@ -275,7 +286,9 @@ class _RoomDetailPageState extends State { SizedBox(width: 3.w), GestureDetector( child: Container( - padding: EdgeInsets.all(3.w), + padding: EdgeInsets.all( + 3.w, + ), child: Image.asset( "sc_images/room/sc_icon_user_card_copy_id.png", width: 11.w, @@ -287,11 +300,11 @@ class _RoomDetailPageState extends State { Clipboard.setData( ClipboardData( text: - ref - .currenRoom - ?.roomProfile - ?.userProfile - ?.account ?? + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.account ?? "", ), ); @@ -342,10 +355,10 @@ class _RoomDetailPageState extends State { Spacer(), widget.isHomeowner ? Icon( - Icons.keyboard_arrow_right_sharp, - color: Colors.black, - size: 15.w, - ) + Icons.keyboard_arrow_right_sharp, + color: Colors.black, + size: 15.w, + ) : Container(), ], ), @@ -356,10 +369,10 @@ class _RoomDetailPageState extends State { return Expanded( child: text( ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomDesc ?? + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomDesc ?? "", fontSize: 13.sp, maxLines: 2, @@ -386,102 +399,104 @@ class _RoomDetailPageState extends State { ), widget.isHomeowner ? Row( - children: [ - SizedBox(width: 15.w), - Expanded( - child: SCDebounceWidget( - child: Container( - height: 45.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 10.w, + children: [ + SizedBox(width: 15.w), + Expanded( + child: SCDebounceWidget( + child: Container( + height: 45.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10.w, + ), + color: SocialChatTheme.primaryLight, + ), + child: Row( + children: [ + SizedBox(width: 20.w), + text( + SCAppLocalizations.of( + context, + )!.roomMember, + textColor: Colors.white, + fontSize: 14.sp, + ), + Spacer(), + Icon( + Icons + .keyboard_arrow_right_sharp, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 15.w), + ], + ), ), - color: SocialChatTheme.primaryLight, - ), - child: Row( - children: [ - SizedBox(width: 20.w), - text( - SCAppLocalizations.of( - context, - )!.roomMember, - textColor: Colors.white, - fontSize: 14.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right_sharp, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 15.w), - ], + onTap: () { + Navigator.of(context).pop(); + showBottomInBottomDialog( + context, + RoomMemberPage( + roomId: + Provider.of( + context!, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + isHomeowner: widget.isHomeowner, + ), + ); + }, ), ), - onTap: () { - Navigator.of(context).pop(); - showBottomInBottomDialog( - context, - RoomMemberPage( - roomId: - Provider.of( - context!, - listen: false, - ) - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id ?? - "", - isHomeowner: widget.isHomeowner, - ), - ); - }, - ), - ), - SizedBox(width: 10.w), - Expanded( - child: SCDebounceWidget( - child: Container( - height: 45.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 10.w, - ), - color: SocialChatTheme.primaryLight, - ), - child: Row( - children: [ - SizedBox(width: 20.w), - text( - SCAppLocalizations.of( - context, - )!.roomEdit, - textColor: Colors.white, - fontSize: 14.sp, + SizedBox(width: 10.w), + Expanded( + child: SCDebounceWidget( + child: Container( + height: 45.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10.w, + ), + color: SocialChatTheme.primaryLight, ), - Spacer(), - Icon( - Icons.keyboard_arrow_right_sharp, - color: Colors.white, - size: 20.w, + child: Row( + children: [ + SizedBox(width: 20.w), + text( + SCAppLocalizations.of( + context, + )!.roomEdit, + textColor: Colors.white, + fontSize: 14.sp, + ), + Spacer(), + Icon( + Icons + .keyboard_arrow_right_sharp, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 15.w), + ], ), - SizedBox(width: 15.w), - ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + }, ), ), - onTap: () { - SCNavigatorUtils.push( - context, - "${VoiceRoomRoute.roomEdit}?need=false", - replace: false, - ); - }, - ), - ), - SizedBox(width: 15.w), - ], - ) + SizedBox(width: 15.w), + ], + ) : Container(), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -489,147 +504,10 @@ class _RoomDetailPageState extends State { children: [ ref.isTourists() ? SCDebounceWidget( - child: Container( - width: 160.w, - decoration: BoxDecoration( - color: SocialChatTheme.primaryLight, - borderRadius: BorderRadius.circular( - 35.w, - ), - ), - height: 42.w, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/room/sc_icon_join_room_member.png", - height: 20.w, - width: 20.w, - ), - SizedBox(width: 8.w), - text( - ref.isTourists() - ? SCAppLocalizations.of( - context, - )!.join - : SCAppLocalizations.of( - context, - )!.giveUpIdentity, - textColor: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - SizedBox(width: 3.w), - text( - "(${ref.currenRoom?.roomProfile?.roomSetting?.joinGolds ?? 0})", - textColor: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - ], - ), - ), - onTap: () { - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: - SCAppLocalizations.of( - context, - )!.tips, - msg: - ref.isTourists() - ? SCAppLocalizations.of( - context, - )!.joinMemberTips2 - : SCAppLocalizations.of( - context, - )!.leaveRoomIdentityTips, - btnText: - SCAppLocalizations.of( - context, - )!.confirm, - onEnsure: () { - SCChatRoomRepository() - .changeRoomRole( - ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id ?? - "", - ref.isTourists() - ? SCRoomRolesType - .ADMIN - .name - : SCRoomRolesType - .TOURIST - .name, - UserManager() - .getCurrentUser() - ?.userProfile - ?.id ?? - "", - UserManager() - .getCurrentUser() - ?.userProfile - ?.id ?? - "", - "ONESELF_JOIN", - ) - .then((reslt) { - ref.currenRoom = ref - .currenRoom - ?.copyWith( - entrants: ref - .currenRoom - ?.entrants - ?.copyWith( - roles: - SCRoomRolesType - .MEMBER - .name, - ), - ); - setState(() {}); - SCTts.show( - SCAppLocalizations.of( - navigatorKey - .currentState! - .context, - )!.operationSuccessful, - ); - }); - }, - ); - }, - ); - }, - ) - : SizedBox.shrink(), - ref.currenRoom?.roomProfile?.roomProfile?.userId == - UserManager() - .getCurrentUser() - ?.userProfile - ?.id - ? SizedBox.shrink() - : Selector( - selector: - (c, p) => - p.isFollowRoomRes?.followRoom ?? - false, - shouldRebuild: (prev, next) => prev != next, - builder: (_, follow, __) { - return SCDebounceWidget( child: Container( width: 160.w, decoration: BoxDecoration( - color: SocialChatTheme.primaryLight, + color: SocialChatTheme.primaryLight, borderRadius: BorderRadius.circular( 35.w, ), @@ -637,186 +515,340 @@ class _RoomDetailPageState extends State { height: 42.w, child: Row( mainAxisAlignment: - MainAxisAlignment.center, + MainAxisAlignment.center, children: [ - Row( - children: [ - Image.asset( - !follow - ? "sc_images/room/sc_icon_follow_room_un.png" - : "sc_images/room/sc_icon_follow_room_en.png", - width: 20.w, - height: 20.w, - ), - SizedBox(width: 8.w), - text( - !follow - ? SCAppLocalizations.of( + Image.asset( + "sc_images/room/sc_icon_join_room_member.png", + height: 20.w, + width: 20.w, + ), + SizedBox(width: 8.w), + text( + ref.isTourists() + ? SCAppLocalizations.of( context, - )!.follow - : SCAppLocalizations.of( + )!.join + : SCAppLocalizations.of( context, - )!.following, - textColor: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - ], + )!.giveUpIdentity, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 3.w), + text( + "(${ref.currenRoom?.roomProfile?.roomSetting?.joinGolds ?? 0})", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.bold, ), ], ), ), onTap: () { - if (follow) { - SmartDialog.show( - tag: "unFollowDialog", - alignment: Alignment.center, - animationType: - SmartAnimationType.fade, - builder: (_) { - return Container( - width: - ScreenUtil().screenWidth * - 0.75, - padding: EdgeInsets.symmetric( - horizontal: 10.w, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.all( - Radius.circular(12.w), - ), - ), - child: Column( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment - .center, + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: + SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context, + )!.tips, + msg: + ref.isTourists() + ? SCAppLocalizations.of( + context, + )!.joinMemberTips2 + : SCAppLocalizations.of( + context, + )!.leaveRoomIdentityTips, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () { + SCChatRoomRepository() + .changeRoomRole( + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + ref.isTourists() + ? SCRoomRolesType + .ADMIN + .name + : SCRoomRolesType + .TOURIST + .name, + UserManager() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + UserManager() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + "ONESELF_JOIN", + ) + .then((reslt) { + ref.currenRoom = ref + .currenRoom + ?.copyWith( + entrants: ref + .currenRoom + ?.entrants + ?.copyWith( + roles: + SCRoomRolesType + .MEMBER + .name, + ), + ); + setState(() {}); + SCTts.show( + SCAppLocalizations.of( + navigatorKey + .currentState! + .context, + )!.operationSuccessful, + ); + }); + }, + ); + }, + ); + }, + ) + : SizedBox.shrink(), + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.userId == + UserManager() + .getCurrentUser() + ?.userProfile + ?.id + ? SizedBox.shrink() + : Selector( + selector: + (c, p) => + p.isFollowRoomRes?.followRoom ?? + false, + shouldRebuild: + (prev, next) => prev != next, + builder: (_, follow, __) { + return SCDebounceWidget( + child: Container( + width: 160.w, + decoration: BoxDecoration( + color: + SocialChatTheme.primaryLight, + borderRadius: + BorderRadius.circular(35.w), + ), + height: 42.w, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Row( children: [ - SizedBox(height: 15.w), + Image.asset( + !follow + ? "sc_images/room/sc_icon_follow_room_un.png" + : "sc_images/room/sc_icon_follow_room_en.png", + width: 20.w, + height: 20.w, + ), + SizedBox(width: 8.w), text( - SCAppLocalizations.of( - context, - )!.unFollow, - fontSize: 16.sp, - textColor: Colors.black, + !follow + ? SCAppLocalizations.of( + context, + )!.follow + : SCAppLocalizations.of( + context, + )!.following, + textColor: Colors.white, + fontSize: 15.sp, fontWeight: - FontWeight.bold, + FontWeight.bold, ), - SizedBox(height: 15.w), - text( - SCAppLocalizations.of( - context, - )!.sureUnfollowThisRoom, - fontWeight: - FontWeight.w600, - textColor: Colors.grey, - fontSize: 14.sp, - ), - SizedBox(height: 15.w), - Row( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment - .center, - children: [ - GestureDetector( - behavior: - HitTestBehavior - .opaque, - onTap: () { - SmartDialog.dismiss( - tag: - "unFollowDialog", - ); - }, - child: Container( - alignment: - AlignmentDirectional - .center, - decoration: - BoxDecoration( - borderRadius: - BorderRadius.circular( - 35.w, - ), - color: Color( - 0xffF2F2F2, - ), - ), - height: 35.w, - width: 95.w, - child: text( - SCAppLocalizations.of( - context, - )!.cancel, - textColor: - Colors.grey, - fontSize: 15.sp, - fontWeight: - FontWeight - .w600, - ), - ), - ), - SizedBox(width: 18.w), - GestureDetector( - onTap: () { - ref.followCurrentVoiceRoom(); - }, - behavior: - HitTestBehavior - .opaque, - child: Container( - alignment: - AlignmentDirectional - .center, - decoration: BoxDecoration( - color: - SocialChatTheme - .primaryLight, - borderRadius: - BorderRadius.circular( - 35.w, - ), - ), - height: 35.w, - width: 95.w, - child: text( - SCAppLocalizations.of( - context, - )!.unFollow, - textColor: - Colors - .white, - fontSize: 15.sp, - fontWeight: - FontWeight - .w600, - ), - ), - ), - ], - ), - SizedBox(height: 25.w), ], ), + ], + ), + ), + onTap: () { + if (follow) { + SmartDialog.show( + tag: "unFollowDialog", + alignment: Alignment.center, + animationType: + SmartAnimationType.fade, + builder: (_) { + return Container( + width: + ScreenUtil() + .screenWidth * + 0.75, + padding: + EdgeInsets.symmetric( + horizontal: 10.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.all( + Radius.circular( + 12.w, + ), + ), + ), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of( + context, + )!.unFollow, + fontSize: 16.sp, + textColor: + Colors.black, + fontWeight: + FontWeight.bold, + ), + SizedBox(height: 15.w), + text( + SCAppLocalizations.of( + context, + )!.sureUnfollowThisRoom, + fontWeight: + FontWeight.w600, + textColor: + Colors.grey, + fontSize: 14.sp, + ), + SizedBox(height: 15.w), + Row( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + GestureDetector( + behavior: + HitTestBehavior + .opaque, + onTap: () { + SmartDialog.dismiss( + tag: + "unFollowDialog", + ); + }, + child: Container( + alignment: + AlignmentDirectional + .center, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 35.w, + ), + color: Color( + 0xffF2F2F2, + ), + ), + height: 35.w, + width: 95.w, + child: text( + SCAppLocalizations.of( + context, + )!.cancel, + textColor: + Colors + .grey, + fontSize: + 15.sp, + fontWeight: + FontWeight + .w600, + ), + ), + ), + SizedBox( + width: 18.w, + ), + GestureDetector( + onTap: () { + ref.followCurrentVoiceRoom(); + }, + behavior: + HitTestBehavior + .opaque, + child: Container( + alignment: + AlignmentDirectional + .center, + decoration: BoxDecoration( + color: + SocialChatTheme + .primaryLight, + borderRadius: + BorderRadius.circular( + 35.w, + ), + ), + height: 35.w, + width: 95.w, + child: text( + SCAppLocalizations.of( + context, + )!.unFollow, + textColor: + Colors + .white, + fontSize: + 15.sp, + fontWeight: + FontWeight + .w600, + ), + ), + ), + ], + ), + SizedBox(height: 25.w), + ], + ), + ); + }, ); - }, - ); - } else { - ref.followCurrentVoiceRoom(); - } + } else { + ref.followCurrentVoiceRoom(); + } + }, + ); }, - ); - }, - ), + ), ], ), - SizedBox(height: 10.w,) + SizedBox(height: 10.w), ], ), ), diff --git a/lib/modules/room/edit/room_edit_page.dart b/lib/modules/room/edit/room_edit_page.dart index 5de4505..1a32b1c 100644 --- a/lib/modules/room/edit/room_edit_page.dart +++ b/lib/modules/room/edit/room_edit_page.dart @@ -1,527 +1,582 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/shared/tools/sc_loading_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; -import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_pick_utils.dart'; -import 'package:yumi/services/room/rc_room_manager.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; -import '../../../shared/tools/sc_lk_dialog_util.dart'; -import '../../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; -import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart'; -import '../../../ui_kit/widgets/room/switch_model/room_mic_switch_page.dart'; -import '../block/blocked_list_page.dart'; -import '../voice_room_route.dart'; - -///编辑房间信息 -class RoomEditPage extends StatefulWidget { - String needRestCurrentRoomInfo = "false"; - - RoomEditPage({super.key, this.needRestCurrentRoomInfo = "false"}); - - @override - _RoomEditPageState createState() => _RoomEditPageState(); -} - -class _RoomEditPageState extends State { - RtcProvider? rtcProvider; - bool isEdit = false; - final TextEditingController _roomNameController = TextEditingController(); - final TextEditingController _roomAnnouncementController = - TextEditingController(); - final int _roomNameMaxLength = 24; - int _roomNameCurrentLength = 0; - String roomCover = ""; - final int _roomAnnouncementMaxLength = 100; - int _roomAnnouncementCurrentLength = 0; - - @override - void initState() { - super.initState(); - rtcProvider = Provider.of(context, listen: false); - _roomNameController.addListener(() { - setState(() { - _roomNameCurrentLength = _getActualCharacterCount( - _roomNameController.text, - ); - }); - }); - _roomAnnouncementController.addListener(() { - setState(() { - _roomAnnouncementCurrentLength = _getActualCharacterCount( - _roomAnnouncementController.text, - ); - }); - }); - _roomNameController.text = - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomName ?? ""; - _roomNameCurrentLength = _getActualCharacterCount(_roomNameController.text); - _roomAnnouncementController.text = - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomDesc ?? ""; - _roomAnnouncementCurrentLength = _getActualCharacterCount( - _roomAnnouncementController.text, - ); - roomCover = - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () { - if (!isEdit) { - SCNavigatorUtils.goBack(context); - return Future.value(false); - } - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context)!.tips, - msg: SCAppLocalizations.of(context)!.theModificationsMade, - onEnsure: () { - SCNavigatorUtils.goBack(context); - }, - ); - }, - ); - return Future.value(false); - }, - child: Stack( - children: [ - Image.asset( - "sc_images/person/sc_icon_edit_userinfo_bg.png", - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.fill, - ), - Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - appBar: SocialChatStandardAppBar( - title: SCAppLocalizations.of(context)!.roomSetting, - onTag: () { - if (widget.needRestCurrentRoomInfo == "true") { - Provider.of(context, listen: false).currenRoom = - null; - } - Navigator.pop(context); - }, - actions: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 5.w), - margin: EdgeInsetsDirectional.only(end: 8.w), - child: text( - SCAppLocalizations.of(context)!.save, - textColor: Colors.white, - fontSize: 15.sp, - ), - ), - onTap: () { - submit(context); - }, - ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 35.w), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 20.w,), - GestureDetector( - child: Stack( - alignment: Alignment.bottomRight, - children: [ - netImage( - url: roomCover, - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - width: 90.w, - height: 90.w, - ), - Icon(Icons.camera_alt, size: 25.w,color: Colors.white,), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - roomCover = url; - }); - } - }); - }, - ), - ], - ), - SizedBox(height: 25.w), - Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomName, - textColor: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15.sp, - ), - ], - ), - SizedBox(height: 10.w), - Row( - children: [ - Expanded( - child: Container( - margin: EdgeInsets.symmetric(horizontal: 25.w), - padding: EdgeInsets.only( - left: width(12), - right: width(12), - ), - alignment: AlignmentDirectional.centerStart, - height: 45.w, - width: ScreenUtil().screenWidth, - decoration: BoxDecoration( - border: Border.all(color: Color(0xff5FFFB7), width: 0.5), - gradient: LinearGradient(colors: [Color(0xff5FFFB7).withOpacity(0.1),Color(0xff5FFFB7).withOpacity(0.6)]), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: TextField( - controller: _roomNameController, - onChanged: (text) { - isEdit = true; - }, - inputFormatters: [ - SCAccurateLengthLimitingTextInputFormatter( - _roomNameMaxLength, - ), - ], - decoration: InputDecoration( - hintText: - SCAppLocalizations.of(context)!.enterRoomName, - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - contentPadding: EdgeInsets.only(top: 0.w), - counterText: '', - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - // enabledBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // focusedBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), - fillColor: Colors.white54, - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3.w), - text( - '$_roomNameCurrentLength/$_roomNameMaxLength', - textColor: Colors.white54, - ), - ], - ), - ), - style: TextStyle( - fontSize: sp(15), - color: Colors.white, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - ), - ], - ), - SizedBox(height: 15.w), - Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomAnnouncement, - textColor: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15.sp, - ), - ], - ), - SizedBox(height: 10.w), - Row( - children: [ - Expanded( - child: Container( - margin: EdgeInsets.symmetric(horizontal: 25.w), - padding: EdgeInsets.only( - top: 5.w, - left: width(12), - right: width(12), - ), - alignment: AlignmentDirectional.centerStart, - height: 110.w, - width: ScreenUtil().screenWidth, - decoration: BoxDecoration( - border: Border.all(color: Color(0xff5FFFB7), width: 0.5), - gradient: LinearGradient(colors: [Color(0xff5FFFB7).withOpacity(0.1),Color(0xff5FFFB7).withOpacity(0.6)]), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: TextField( - controller: _roomAnnouncementController, - onChanged: (text) { - isEdit = true; - }, - inputFormatters: [ - SCAccurateLengthLimitingTextInputFormatter( - _roomAnnouncementMaxLength, - ), - ], - maxLines: 5, - decoration: InputDecoration( - hintText: "", - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - contentPadding: EdgeInsets.only(top: 0.w), - counterText: '', - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - // enabledBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // focusedBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), - fillColor: Colors.white54, - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3.w), - text( - '$_roomAnnouncementCurrentLength/$_roomAnnouncementMaxLength', - textColor: Colors.white54, - ), - ], - ), - ), - style: TextStyle( - fontSize: sp(15), - color: Colors.white, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - ), - ], - ), - widget.needRestCurrentRoomInfo != "true" - ? Column( - children: [ - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomTheme2, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - SCNavigatorUtils.push( - context, - VoiceRoomRoute.roomTheme, - replace: false, - ); - }, - ), - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.micManagement, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - SmartDialog.show( - tag: "showRoomMicSwitch", - alignment: Alignment.bottomCenter, - animationType: SmartAnimationType.fade, - debounce: true, - builder: (_) { - return RoomMicSwitchPage(); - }, - ); - }, - ), - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.blockedList2, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - showBottomInBottomDialog( - context, - BlockedListPage( - roomId: - Provider.of( - context!, - listen: false, - ) - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id ?? - "", - ), - ); - }, - ), - SizedBox(height: 15.w), - ], - ) - : Container(), - ], - ), - ), - ), - ], - ), - ); - } - - int _getActualCharacterCount(String text) { - return text.characters.length; - } - - void submit(BuildContext context) async { - if (_roomNameController.text.trim().isEmpty) { - SCTts.show("Room name not empty!"); - return; - } - if (_roomAnnouncementController.text.trim().isEmpty) { - SCTts.show("Room announcement not empty!"); - return; - } - SCLoadingManager.show(context: context); - var roomInfo = await SCAccountRepository().editRoomInfo( - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", - roomCover, - _roomNameController.text, - _roomAnnouncementController.text, - SCRoomInfoEventType.AVAILABLE.name, - ); - Provider.of( - context, - listen: false, - ).updateMyRoomInfo(roomInfo); - if (widget.needRestCurrentRoomInfo == "true") { - ///需要创建群组 - var c = await Provider.of( - context, - listen: false, - ).createRoomGroup( - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomName ?? "", - ); - if (c.code == 0) { - SCLoadingManager.hide(); - SCNavigatorUtils.goBack(context); - Provider.of( - context, - listen: false, - ).joinVoiceRoomSession(context, roomInfo.id ?? "", clearRoomData: true); - } else { - SCLoadingManager.hide(); - SCTts.show("${c.code}"); - } - } else { - Provider.of( - context, - listen: false, - ).loadRoomInfo(roomInfo.id ?? ""); - SCLoadingManager.hide(); - SCNavigatorUtils.goBack(context); - } - } - -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/services/room/rc_room_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import '../../../shared/tools/sc_lk_dialog_util.dart'; +import '../../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; +import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart'; +import '../../../ui_kit/widgets/room/switch_model/room_mic_switch_page.dart'; +import '../block/blocked_list_page.dart'; +import '../voice_room_route.dart'; + +///编辑房间信息 +class RoomEditPage extends StatefulWidget { + final String needRestCurrentRoomInfo; + + const RoomEditPage({super.key, this.needRestCurrentRoomInfo = "false"}); + + @override + State createState() => _RoomEditPageState(); +} + +class _RoomEditPageState extends State { + RtcProvider? rtcProvider; + bool isEdit = false; + final TextEditingController _roomNameController = TextEditingController(); + final TextEditingController _roomAnnouncementController = + TextEditingController(); + final int _roomNameMaxLength = 24; + int _roomNameCurrentLength = 0; + String roomCover = ""; + final int _roomAnnouncementMaxLength = 100; + int _roomAnnouncementCurrentLength = 0; + + @override + void initState() { + super.initState(); + rtcProvider = Provider.of(context, listen: false); + _roomNameController.addListener(() { + setState(() { + _roomNameCurrentLength = _getActualCharacterCount( + _roomNameController.text, + ); + }); + }); + _roomAnnouncementController.addListener(() { + setState(() { + _roomAnnouncementCurrentLength = _getActualCharacterCount( + _roomAnnouncementController.text, + ); + }); + }); + _roomNameController.text = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomName ?? ""; + _roomNameCurrentLength = _getActualCharacterCount(_roomNameController.text); + _roomAnnouncementController.text = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomDesc ?? ""; + _roomAnnouncementCurrentLength = _getActualCharacterCount( + _roomAnnouncementController.text, + ); + roomCover = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (!isEdit) { + SCNavigatorUtils.goBack(context); + return Future.value(false); + } + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.theModificationsMade, + onEnsure: () { + SCNavigatorUtils.goBack(context); + }, + ); + }, + ); + return Future.value(false); + }, + child: Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.roomSetting, + onTag: () { + if (widget.needRestCurrentRoomInfo == "true") { + Provider.of(context, listen: false).currenRoom = + null; + } + Navigator.pop(context); + }, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + margin: EdgeInsetsDirectional.only(end: 8.w), + child: text( + SCAppLocalizations.of(context)!.save, + textColor: Colors.white, + fontSize: 15.sp, + ), + ), + onTap: () { + submit(context); + }, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 35.w), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 20.w), + GestureDetector( + child: Stack( + alignment: Alignment.bottomRight, + children: [ + netImage( + url: roomCover, + defaultImg: kRoomCoverDefaultImg, + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + width: 90.w, + height: 90.w, + ), + Icon( + Icons.camera_alt, + size: 25.w, + color: Colors.white, + ), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + debugPrint("[Room Cover] uploaded url: $url"); + setState(() { + roomCover = url; + isEdit = true; + }); + } + }); + }, + ), + ], + ), + SizedBox(height: 25.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomName, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15.sp, + ), + ], + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 25.w), + padding: EdgeInsets.only( + left: width(12), + right: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 45.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + border: Border.all( + color: Color(0xff5FFFB7), + width: 0.5, + ), + gradient: LinearGradient( + colors: [ + Color(0xff5FFFB7).withOpacity(0.1), + Color(0xff5FFFB7).withOpacity(0.6), + ], + ), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: TextField( + controller: _roomNameController, + onChanged: (text) { + isEdit = true; + }, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter( + _roomNameMaxLength, + ), + ], + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterRoomName, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + text( + '$_roomNameCurrentLength/$_roomNameMaxLength', + textColor: Colors.white54, + ), + ], + ), + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ], + ), + SizedBox(height: 15.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomAnnouncement, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15.sp, + ), + ], + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 25.w), + padding: EdgeInsets.only( + top: 5.w, + left: width(12), + right: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 110.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + border: Border.all( + color: Color(0xff5FFFB7), + width: 0.5, + ), + gradient: LinearGradient( + colors: [ + Color(0xff5FFFB7).withOpacity(0.1), + Color(0xff5FFFB7).withOpacity(0.6), + ], + ), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: TextField( + controller: _roomAnnouncementController, + onChanged: (text) { + isEdit = true; + }, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter( + _roomAnnouncementMaxLength, + ), + ], + maxLines: 5, + decoration: InputDecoration( + hintText: "", + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + text( + '$_roomAnnouncementCurrentLength/$_roomAnnouncementMaxLength', + textColor: Colors.white54, + ), + ], + ), + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ], + ), + widget.needRestCurrentRoomInfo != "true" + ? Column( + children: [ + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomTheme2, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + VoiceRoomRoute.roomTheme, + replace: false, + ); + }, + ), + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.micManagement, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + SmartDialog.show( + tag: "showRoomMicSwitch", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + debounce: true, + builder: (_) { + return RoomMicSwitchPage(); + }, + ); + }, + ), + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.blockedList2, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + showBottomInBottomDialog( + context, + BlockedListPage( + roomId: + Provider.of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + ), + ); + }, + ), + SizedBox(height: 15.w), + ], + ) + : Container(), + ], + ), + ), + ), + ], + ), + ); + } + + int _getActualCharacterCount(String text) { + return text.characters.length; + } + + String _preferNonEmpty(String? primary, String fallback) { + if ((primary ?? "").trim().isNotEmpty) { + return primary!; + } + return fallback; + } + + void submit(BuildContext context) async { + final roomManager = Provider.of( + context, + listen: false, + ); + final currentRtcProvider = Provider.of(context, listen: false); + final currentRtmProvider = Provider.of(context, listen: false); + final submittedRoomName = _roomNameController.text.trim(); + final submittedRoomDesc = _roomAnnouncementController.text.trim(); + final submittedRoomCover = roomCover.trim(); + if (submittedRoomName.isEmpty) { + SCTts.show("Room name not empty!"); + return; + } + if (submittedRoomDesc.isEmpty) { + SCTts.show("Room announcement not empty!"); + return; + } + SCLoadingManager.show(context: context); + debugPrint("[Room Cover] submit roomCover: $submittedRoomCover"); + var roomInfo = await SCAccountRepository().editRoomInfo( + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + submittedRoomCover, + submittedRoomName, + submittedRoomDesc, + SCRoomInfoEventType.AVAILABLE.name, + ); + debugPrint("[Room Cover] editRoomInfo response: ${roomInfo.toJson()}"); + if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + final mergedRoomInfo = roomInfo.copyWith( + roomCover: _preferNonEmpty(roomInfo.roomCover, submittedRoomCover), + roomName: _preferNonEmpty(roomInfo.roomName, submittedRoomName), + roomDesc: _preferNonEmpty(roomInfo.roomDesc, submittedRoomDesc), + ); + currentRtcProvider.updateCurrentRoomBasicInfo( + roomCover: mergedRoomInfo.roomCover, + roomName: mergedRoomInfo.roomName, + roomDesc: mergedRoomInfo.roomDesc, + ); + roomManager.updateMyRoomInfo(mergedRoomInfo); + if (widget.needRestCurrentRoomInfo == "true") { + ///需要创建群组 + var c = await currentRtmProvider.createRoomGroup( + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + mergedRoomInfo.roomName ?? submittedRoomName, + ); + if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + if (c.code == 0) { + SCLoadingManager.hide(); + SCNavigatorUtils.goBack(context); + currentRtcProvider.joinVoiceRoomSession( + context, + mergedRoomInfo.id ?? + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? + "", + clearRoomData: true, + ); + } else { + SCLoadingManager.hide(); + SCTts.show("${c.code}"); + } + } else { + currentRtcProvider.loadRoomInfo( + mergedRoomInfo.id ?? + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? + "", + ); + SCLoadingManager.hide(); + SCNavigatorUtils.goBack(context); + } + } +} diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 9563260..f995cea 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -51,7 +51,9 @@ class _VoiceRoomPageState extends State _floatingGiftListener; _tabController.addListener(() {}); // 监听切换 - _subscription = eventBus.on().listen((event) { + _subscription = eventBus.on().listen(( + event, + ) { if (mounted) { Provider.of(context, listen: false).clearAllGiftData(); Provider.of( @@ -64,6 +66,10 @@ class _VoiceRoomPageState extends State @override void dispose() { + final rtmProvider = Provider.of(context, listen: false); + if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) { + rtmProvider.msgFloatingGiftListener = null; + } _tabController.dispose(); // 释放资源 _subscription.cancel(); super.dispose(); @@ -132,7 +138,7 @@ class _VoiceRoomPageState extends State SizedBox(height: ScreenUtil().setWidth(5)), RoomOnlineUserWidget(), RoomSeatWidget(), - SizedBox(height: 2.w,), + SizedBox(height: 2.w), Expanded( child: Stack( children: [ diff --git a/lib/modules/search/sc_search_page.dart b/lib/modules/search/sc_search_page.dart index 419fc4b..f507b07 100644 --- a/lib/modules/search/sc_search_page.dart +++ b/lib/modules/search/sc_search_page.dart @@ -452,7 +452,8 @@ class _SearchRoomListState extends State { ), ), child: netImage( - url: e.roomCover ?? "", + url: resolveRoomCoverUrl(e.id, e.roomCover), + defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.circular(12.w), width: 200.w, height: 200.w, diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 45a0aea..93cc213 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -160,6 +160,501 @@ class _PersonDetailPageState extends State return Tab(text: text); } + Widget _buildProfileHeader( + SocialChatUserProfileManager ref, + List backgroundPhotos, + ) { + return SizedBox( + height: 450.w, + child: Stack( + children: [ + SizedBox( + height: 300.w, + width: ScreenUtil().screenWidth, + child: + backgroundPhotos.isNotEmpty + ? CarouselSlider( + options: CarouselOptions( + height: 300.w, + autoPlay: backgroundPhotos.length > 1, + enlargeCenterPage: false, + aspectRatio: 1 / 1, + enableInfiniteScroll: true, + autoPlayAnimationDuration: Duration(milliseconds: 800), + viewportFraction: 1, + onPageChanged: (index, reason) { + setState(() {}); + }, + ), + items: + backgroundPhotos.map((item) { + return GestureDetector( + child: netImage( + url: item.url ?? "", + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + onTap: () {}, + ); + }).toList(), + ) + : Image.asset( + 'sc_images/person/sc_icon_my_head_bg_defalt.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 250.w, + left: 0, + right: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + color: Color(0xff083b2f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.w), + topRight: Radius.circular(8.w), + ), + ), + ), + ), + Positioned( + top: 210.w, + left: 0, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox(width: 15.w), + (ref.userProfile?.cpList?.isNotEmpty ?? false) + ? SizedBox( + height: 100.w, + child: Stack( + alignment: Alignment.center, + children: [ + Transform.translate( + offset: Offset(0, -5), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + child: netImage( + url: + ref + .userProfile + ?.cpList + ?.first + .meUserAvatar ?? + "", + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + width: 56.w, + shape: BoxShape.circle, + ), + onTap: () { + String encodedUrls = + Uri.encodeComponent( + jsonEncode([ + ref + .userProfile + ?.cpList + ?.first + .meUserAvatar, + ]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + SizedBox(width: 32.w), + GestureDetector( + onTap: () { + SCNavigatorUtils.push( + context, + replace: true, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}", + ); + }, + child: netImage( + url: + ref + .userProfile + ?.cpList + ?.first + .cpUserAvatar ?? + "", + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + width: 56.w, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + IgnorePointer( + child: Transform.translate( + offset: Offset(0, -15), + child: Image.asset( + "sc_images/person/sc_icon_send_cp_requst_dialog_head.png", + ), + ), + ), + ], + ), + ) + : GestureDetector( + child: head( + url: ref.userProfile?.userAvatar ?? "", + width: 88.w, + headdress: + ref.userProfile?.getHeaddress()?.sourceUrl, + ), + onTap: () { + String encodedUrls = Uri.encodeComponent( + jsonEncode([ref.userProfile?.userAvatar]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + ], + ), + SizedBox(height: 6.w), + Row( + children: [ + SizedBox(width: 25.w), + socialchatNickNameText( + maxWidth: 135.w, + ref.userProfile?.userNickname ?? "", + fontSize: 18.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref.userProfile?.userNickname?.characters.length ?? + 0) > + 13, + ), + SizedBox(width: 3.w), + getVIPBadge( + ref.userProfile?.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + Row( + children: [ + SizedBox(width: 5.w), + Container( + width: + (ref.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + ref.userProfile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(ref.userProfile?.userSex), + text( + "${ref.userProfile?.age}", + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 3.w), + ], + ), + ), + ], + ), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + "ID:${ref.userProfile?.account ?? ""}", + textColor: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 16.sp, + ), + ], + ), + SizedBox(height: 5.w), + Consumer( + builder: (context, ref, child) { + return Container( + height: 72.w, + alignment: AlignmentDirectional.center, + width: ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric(horizontal: 18.w), + padding: EdgeInsets.symmetric(horizontal: 35.w), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["INTERVIEW"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.vistors, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.vistors, + ); + } + }, + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff333333).withOpacity(0.0), + Color(0xff333333), + Color(0xff333333).withOpacity(0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.follow, + ); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.follow, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff333333).withOpacity(0.0), + Color(0xff333333), + Color(0xff333333).withOpacity(0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["FANS"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.fans, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.fans, + ); + } + }, + ), + ], + ), + ); + }, + ), + SizedBox(height: 6.w), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSkeletonPiece({ + double? width, + required double height, + BorderRadius? borderRadius, + BoxShape shape = BoxShape.rectangle, + }) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.14), + borderRadius: + shape == BoxShape.circle + ? null + : (borderRadius ?? BorderRadius.circular(8.w)), + shape: shape, + ), + ); + } + + Widget _buildProfileSkeleton() { + return SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 210.w), + Padding( + padding: EdgeInsets.only(left: 15.w), + child: _buildSkeletonPiece( + width: 88.w, + height: 88.w, + shape: BoxShape.circle, + ), + ), + SizedBox(height: 10.w), + Padding( + padding: EdgeInsets.only(left: 25.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSkeletonPiece(width: 150.w, height: 24.w), + SizedBox(height: 10.w), + _buildSkeletonPiece(width: 110.w, height: 18.w), + ], + ), + ), + SizedBox(height: 12.w), + Container( + height: 72.w, + alignment: AlignmentDirectional.center, + width: ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric(horizontal: 18.w), + padding: EdgeInsets.symmetric(horizontal: 35.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate( + 3, + (_) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSkeletonPiece(width: 24.w, height: 22.w), + SizedBox(height: 8.w), + _buildSkeletonPiece(width: 58.w, height: 14.w), + ], + ), + ), + ), + ), + SizedBox(height: 6.w), + Container( + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xff083b2f), Color(0xff083b2f)], + begin: AlignmentDirectional.topStart, + end: AlignmentDirectional.bottomEnd, + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Row( + children: [ + _buildSkeletonPiece(width: 92.w, height: 18.w), + SizedBox(width: 14.w), + _buildSkeletonPiece(width: 72.w, height: 18.w), + ], + ), + ), + SizedBox(height: 24.w), + Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSkeletonPiece(width: 100.w, height: 22.w), + SizedBox(height: 18.w), + _buildSkeletonPiece(width: 220.w, height: 16.w), + SizedBox(height: 14.w), + _buildSkeletonPiece(width: 190.w, height: 16.w), + SizedBox(height: 14.w), + _buildSkeletonPiece(width: 240.w, height: 16.w), + SizedBox(height: 14.w), + _buildSkeletonPiece(width: 200.w, height: 16.w), + SizedBox(height: 14.w), + _buildSkeletonPiece(width: 160.w, height: 16.w), + ], + ), + ), + SizedBox(height: 220.w), + ], + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { _tabs.clear(); @@ -169,9 +664,12 @@ class _PersonDetailPageState extends State // _tabs.add(_buildTab(3, SCAppLocalizations.of(context)!.relationShip)); return Consumer( builder: (context, ref, child) { + final bool isProfileLoading = ref.userProfile == null; List backgroundPhotos = []; backgroundPhotos = - (ref.userProfile?.backgroundPhotos ?? []) + (isProfileLoading + ? [] + : (ref.userProfile?.backgroundPhotos ?? [])) .where((t) => t.status == 1) .toList(); return Directionality( @@ -183,92 +681,35 @@ class _PersonDetailPageState extends State top: false, child: Stack( children: [ - backgroundPhotos.isNotEmpty - ? (isBlacklistLoading || isBlacklist - ? Container() - : Column( - children: [ - SizedBox( - height: 300.w, - child: CarouselSlider( - options: CarouselOptions( + Positioned.fill( + child: Column( + children: [ + SizedBox( + height: 300.w, + width: ScreenUtil().screenWidth, + child: + backgroundPhotos.isNotEmpty + ? netImage( + url: backgroundPhotos.first.url ?? "", + width: ScreenUtil().screenWidth, height: 300.w, - autoPlay: backgroundPhotos.length > 1, - // 启用自动播放 - enlargeCenterPage: false, - // 居中放大当前页面 - aspectRatio: 1 / 1, - // 宽高比 - enableInfiniteScroll: true, - // 启用无限循环 - autoPlayAnimationDuration: Duration( - milliseconds: 800, - ), - // 自动播放动画时长 - viewportFraction: 1, - // 视口分数 - onPageChanged: (index, reason) { - setState(() {}); - }, + fit: BoxFit.cover, + ) + : Image.asset( + 'sc_images/person/sc_icon_my_head_bg_defalt.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, ), - items: - backgroundPhotos.map((item) { - return GestureDetector( - child: netImage( - url: item.url ?? "", - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), - onTap: () {}, - ); - }).toList(), - ), - ), - Transform.translate( - offset: Offset(0, -6.w), - child: Container( - decoration: BoxDecoration( - color: Color(0xff083b2f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.w), - topRight: Radius.circular(8.w), - ), - ), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - ), - ), - ], - )) - : Column( - children: [ - SizedBox( - height: 300.w, - child: Image.asset( - 'sc_images/person/sc_icon_my_head_bg_defalt.png', - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), - ), - Transform.translate( - offset: Offset(0, -6.w), - child: Container( - decoration: BoxDecoration( - color: Color(0xff083b2f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.w), - topRight: Radius.circular(8.w), - ), - ), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - ), - ), - ], - ), + ), + Expanded( + child: Container(color: const Color(0xff083b2f)), + ), + ], + ), + ), Scaffold( + extendBodyBehindAppBar: true, resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, appBar: SocialChatStandardAppBar( @@ -295,13 +736,12 @@ class _PersonDetailPageState extends State ), actions: [ // 在AppBar中显示头像和昵称 - if (_opacity > 0.5) ...[ + if (_opacity > 0.5 && !isProfileLoading) ...[ SizedBox(width: 45.w), head( url: ref.userProfile?.userAvatar ?? "", width: 50.w, height: 50.w, - headdress: ref.userProfile?.getHeaddress()?.sourceUrl, ), SizedBox(width: 8.w), socialchatNickNameText( @@ -594,552 +1034,135 @@ class _PersonDetailPageState extends State ], ), body: - isBlacklistLoading || isBlacklist + isBlacklist ? Container() - : ExtendedNestedScrollView( - controller: _scrollController, - onlyOneScrollInBody: true, - headerSliverBuilder: ( - BuildContext context, - bool innerBoxIsScrolled, - ) { - return [ - SliverToBoxAdapter( - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox(height: 210.w), // 给AppBar留出空间 - Column( - children: [ - Row( - children: [ - SizedBox(width: 15.w), - (ref - .userProfile - ?.cpList - ?.isNotEmpty ?? - false) - ? SizedBox( - height: 100.w, - child: Stack( - alignment: - Alignment.center, - children: [ - Transform.translate( - offset: Offset( - 0, - -5, - ), - child: Row( - mainAxisSize: - MainAxisSize - .min, - crossAxisAlignment: - CrossAxisAlignment - .center, - children: [ - GestureDetector( - child: netImage( - url: - ref - .userProfile - ?.cpList - ?.first - .meUserAvatar ?? - "", - defaultImg: - "sc_images/general/sc_icon_avar_defalt.png", - width: 56.w, - shape: - BoxShape - .circle, - ), - onTap: () { - String - encodedUrls = Uri.encodeComponent( - jsonEncode([ - ref - .userProfile - ?.cpList - ?.first - .meUserAvatar, - ]), - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", - ); - }, + : (isProfileLoading || isBlacklistLoading + ? _buildProfileSkeleton() + : ExtendedNestedScrollView( + controller: _scrollController, + onlyOneScrollInBody: true, + headerSliverBuilder: ( + BuildContext context, + bool innerBoxIsScrolled, + ) { + return [ + SliverToBoxAdapter( + child: _buildProfileHeader( + ref, + backgroundPhotos, + ), + ), + ]; + }, + body: Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + decoration: BoxDecoration( + gradient: + ref.userProfile?.userSex == 0 + ? LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: + AlignmentDirectional.topStart, + end: + AlignmentDirectional + .bottomEnd, + ) + : LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: + AlignmentDirectional.topStart, + end: + AlignmentDirectional + .bottomEnd, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 6.w), + SizedBox( + height: 35.w, + child: TabBar( + tabAlignment: TabAlignment.start, + indicator: + SCFixedWidthTabIndicator( + width: 20.w, + height: 4.w, + gradient: + ref + .userProfile + ?.userSex == + 0 + ? LinearGradient( + colors: [ + Color( + 0xffFFD800, ), - SizedBox( - width: 32.w, + Color( + 0xffFFD800, ), - GestureDetector( - onTap: () { - SCNavigatorUtils.push( - context, - replace: - true, - "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}", - ); - }, - child: netImage( - url: - ref - .userProfile - ?.cpList - ?.first - .cpUserAvatar ?? - "", - defaultImg: - "sc_images/general/sc_icon_avar_defalt.png", - width: 56.w, - shape: - BoxShape - .circle, - ), + ], + ) + : LinearGradient( + colors: [ + Color( + 0xffFFD800, + ), + Color( + 0xffFFD800, ), ], ), - ), - IgnorePointer( - child: Transform.translate( - offset: Offset( - 0, - -15, - ), - child: Image.asset( - "sc_images/person/sc_icon_send_cp_requst_dialog_head.png", - ), - ), - ), - ], - ), - ) - : GestureDetector( - child: head( - url: - ref - .userProfile - ?.userAvatar ?? - "", - width: 88.w, - headdress: - ref.userProfile - ?.getHeaddress() - ?.sourceUrl, - ), - onTap: () { - String encodedUrls = - Uri.encodeComponent( - jsonEncode([ - ref - .userProfile - ?.userAvatar, - ]), - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", - ); - }, - ), - ], - ), - SizedBox(height: 6.w), - Row( - children: [ - SizedBox(width: 25.w), - socialchatNickNameText( - maxWidth: 135.w, - ref - .userProfile - ?.userNickname ?? - "", - fontSize: 18.sp, - textColor: Colors.white, - fontWeight: FontWeight.w600, - type: - ref.userProfile - ?.getVIP() - ?.name ?? - "", - needScroll: - (ref - .userProfile - ?.userNickname - ?.characters - .length ?? - 0) > - 13, - ), - SizedBox(width: 3.w), - getVIPBadge( - ref.userProfile - ?.getVIP() - ?.name, - width: 45.w, - height: 25.w, - ), - Row( - children: [ - SizedBox(width: 5.w), - Container( - width: - (ref.userProfile?.age ?? - 0) > - 999 - ? 58.w - : 48.w, - height: 24.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - ref.userProfile?.userSex == - 0 - ? "sc_images/login/sc_icon_sex_woman_bg.png" - : "sc_images/login/sc_icon_sex_man_bg.png", - ), - ), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment - .center, - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - xb( - ref - .userProfile - ?.userSex, - ), - text( - "${ref.userProfile?.age}", - textColor: - Colors.white, - fontSize: 14.sp, - fontWeight: - FontWeight.w600, - ), - SizedBox(width: 3.w), - ], - ), - ), - ], - ), - ], - ), - SizedBox(height: 5.w), - Row( - children: [ - SizedBox(width: 25.w), - text( - "ID:${ref.userProfile?.account ?? ""}", - textColor: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 16.sp, - ), - ], - ), - SizedBox(height: 5.w), - Consumer< - SocialChatUserProfileManager - >( - builder: (context, ref, child) { - return Container( - height: 72.w, - alignment: - AlignmentDirectional - .center, - width: - ScreenUtil().screenWidth, - margin: EdgeInsets.symmetric( - horizontal: 18.w, ), - padding: EdgeInsets.symmetric( - horizontal: 35.w, + labelPadding: + EdgeInsets.symmetric( + horizontal: 12.w, ), - child: Row( - mainAxisSize: - MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - GestureDetector( - child: Column( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - text( - "${counterMap["INTERVIEW"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: - Colors.white, - fontWeight: - FontWeight - .bold, - ), - text( - SCAppLocalizations.of( - context, - )!.vistors, - fontSize: 14.sp, - textColor: Color( - 0xffB1B1B1, - ), - ), - ], - ), - onTap: () { - if (widget.isMe == - "true") { - SCNavigatorUtils.push( - context, - SCMainRoute - .vistors, - ); - } - }, - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color( - 0xff333333, - ).withOpacity( - 0.0, - ), - Color(0xff333333), - Color( - 0xff333333, - ).withOpacity( - 0.0, - ), - ], - begin: - Alignment - .topCenter, - end: - Alignment - .bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - onTap: () { - if (widget.isMe == - "true") { - SCNavigatorUtils.push( - context, - SCMainRoute - .follow, - ); - } - }, - child: Column( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - text( - "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: - Colors.white, - fontWeight: - FontWeight - .bold, - ), - text( - SCAppLocalizations.of( - context, - )!.follow, - fontSize: 14.sp, - textColor: Color( - 0xffB1B1B1, - ), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color( - 0xff333333, - ).withOpacity( - 0.0, - ), - Color(0xff333333), - Color( - 0xff333333, - ).withOpacity( - 0.0, - ), - ], - begin: - Alignment - .topCenter, - end: - Alignment - .bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - child: Column( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - text( - "${counterMap["FANS"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: - Colors.white, - fontWeight: - FontWeight - .bold, - ), - text( - SCAppLocalizations.of( - context, - )!.fans, - fontSize: 14.sp, - textColor: Color( - 0xffB1B1B1, - ), - ), - ], - ), - onTap: () { - if (widget.isMe == - "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.fans, - ); - } - }, - ), - ], - ), - ); - }, + labelColor: Colors.white, + isScrollable: true, + unselectedLabelColor: Color( + 0xffB1B1B1, + ), + labelStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + ), + indicatorColor: + Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, ), - SizedBox(height: 6.w), - ], - ), - ], - ), - ), - ), - ]; - }, - body: Container( - padding: EdgeInsets.symmetric(horizontal: 3.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.w), - topRight: Radius.circular(8.w), - ), - gradient: - ref.userProfile?.userSex == 0 - ? LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: AlignmentDirectional.topStart, - end: AlignmentDirectional.bottomEnd, - ) - : LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: AlignmentDirectional.topStart, - end: AlignmentDirectional.bottomEnd, - ), - ), - child: Column( - children: [ - Row( - children: [ - SizedBox(width: 6.w), - SizedBox( - height: 35.w, - child: TabBar( - tabAlignment: TabAlignment.start, - indicator: SCFixedWidthTabIndicator( - width: 20.w, - height: 4.w, - gradient: - ref.userProfile?.userSex == 0 - ? LinearGradient( - colors: [ - Color(0xffFFD800), - Color(0xffFFD800), - ], - ) - : LinearGradient( - colors: [ - Color(0xffFFD800), - Color(0xffFFD800), - ], - ), ), - labelPadding: EdgeInsets.symmetric( - horizontal: 12.w, - ), - labelColor: Colors.white, - isScrollable: true, - unselectedLabelColor: Color( - 0xffB1B1B1, - ), - labelStyle: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: TextStyle( - fontSize: 13.sp, - ), - indicatorColor: Colors.transparent, - dividerColor: Colors.transparent, + SizedBox(width: 6.w), + ], + ), + Expanded( + child: TabBarView( controller: _tabController, - tabs: _tabs, + children: _pages, ), ), - SizedBox(width: 6.w), + SizedBox(height: 25.w), ], ), - Expanded( - child: TabBarView( - controller: _tabController, - children: _pages, - ), - ), - SizedBox(height: 25.w), - ], - ), - ), - ), + ), + )), ), // 底部按钮区域 - isBlacklistLoading || isBlacklist + isBlacklistLoading || isBlacklist || isProfileLoading ? Container() : Positioned( bottom: 0, @@ -1191,7 +1214,9 @@ class _PersonDetailPageState extends State SCAppLocalizations.of( context, )!.inRoom, - textColor: SocialChatTheme.primaryLight, + textColor: + SocialChatTheme + .primaryLight, fontWeight: FontWeight.w600, fontSize: 14.sp, @@ -1238,7 +1263,9 @@ class _PersonDetailPageState extends State SCAppLocalizations.of( context, )!.message, - textColor: SocialChatTheme.primaryLight, + textColor: + SocialChatTheme + .primaryLight, fontWeight: FontWeight.w600, fontSize: 14.sp, @@ -1302,7 +1329,9 @@ class _PersonDetailPageState extends State : SCAppLocalizations.of( context, )!.follow, - textColor: SocialChatTheme.primaryLight, + textColor: + SocialChatTheme + .primaryLight, fontWeight: FontWeight.w600, fontSize: 14.sp, diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 92f6684..e7edbea 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -37,6 +37,7 @@ import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import '../../shared/tools/sc_room_profile_cache.dart'; import '../../ui_kit/components/sc_float_ichart.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); @@ -49,7 +50,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { JoinRoomRes? currenRoom; /// 声音音量变化监听 - List _onSoundVoiceChangeList = []; + final List _onSoundVoiceChangeList = []; ///麦位 Map roomWheatMap = {}; @@ -240,7 +241,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (num.parse(v.user!.account!) == uid) { roomWheatMap[k]!.setVolume = voloum; for (var element in _onSoundVoiceChangeList) { - element?.call(k, voloum); + element(k, voloum); } } } @@ -267,6 +268,43 @@ class RealTimeCommunicationManager extends ChangeNotifier { int startTime = 0; + String? _preferNonEmpty(String? primary, String? fallback) { + if ((primary ?? "").trim().isNotEmpty) { + return primary; + } + if ((fallback ?? "").trim().isNotEmpty) { + return fallback; + } + return primary ?? fallback; + } + + void updateCurrentRoomBasicInfo({ + String? roomCover, + String? roomName, + String? roomDesc, + }) { + final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; + if (currenRoom == null || currentRoomProfile == null) { + return; + } + currenRoom = currenRoom?.copyWith( + roomProfile: currenRoom?.roomProfile?.copyWith( + roomProfile: currentRoomProfile.copyWith( + roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover), + roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), + roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), + ), + ), + ); + SCRoomProfileCache.saveRoomProfile( + roomId: currentRoomProfile.id ?? "", + roomCover: roomCover, + roomName: roomName, + roomDesc: roomDesc, + ); + notifyListeners(); + } + void joinVoiceRoomSession( BuildContext context, String roomId, { @@ -897,14 +935,24 @@ class RealTimeCommunicationManager extends ChangeNotifier { Future loadRoomInfo(String roomId) async { try { final value = await SCChatRoomRepository().specific(roomId); + final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( - roomCounter: value.counter, - roomSetting: value.setting, - roomProfile: currenRoom?.roomProfile?.roomProfile?.copyWith( - roomCover: value.roomCover, - roomName: value.roomName, - roomDesc: value.roomDesc, + roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, + roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting, + roomProfile: currentRoomProfile?.copyWith( + roomCover: _preferNonEmpty( + value.roomCover, + currentRoomProfile.roomCover, + ), + roomName: _preferNonEmpty( + value.roomName, + currentRoomProfile.roomName, + ), + roomDesc: _preferNonEmpty( + value.roomDesc, + currentRoomProfile.roomDesc, + ), ), ), ); @@ -1073,6 +1121,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (index > -1) { return seatGlobalKeyMap[index]; } + return null; } ///播放音乐 diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index cda221d..8b42ff5 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -16,12 +16,12 @@ import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.d import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:provider/provider.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; -import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; @@ -50,7 +50,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_pu import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; @@ -71,6 +71,10 @@ typedef RtmProvider = RealTimeMessagingManager; class RealTimeMessagingManager extends ChangeNotifier { BuildContext? context; + void _giftFxLog(String message) { + debugPrint('[GiftFX][RTM] $message'); + } + ///消息列表 List roomAllMsgList = []; List roomChatMsgList = []; @@ -113,48 +117,48 @@ class RealTimeMessagingManager extends ChangeNotifier { Debouncer debouncer = Debouncer(); List conversationList = []; - ///客服 - SocialChatUserProfile? customerInfo; - - int _systemUnreadCount() { - int count = 0; - for (final conversationId in SCGlobalConfig.systemConversationIds) { - count += conversationMap[conversationId]?.unreadCount ?? 0; - } - return count; - } - - V2TimConversation getPreferredSystemConversation() { - final systemConversations = - conversationMap.values - .where( - (element) => - SCGlobalConfig.isSystemConversationId(element.conversationID), - ) - .toList() - ..sort((e1, e2) { - final time1 = e1.lastMessage?.timestamp ?? 0; - final time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - - if (systemConversations.isNotEmpty) { - return systemConversations.first; - } - - return V2TimConversation( - type: ConversationType.V2TIM_C2C, - userID: SCGlobalConfig.primarySystemUserId, - conversationID: SCGlobalConfig.primarySystemConversationId, - ); - } - - void getConversationList() { - List list = conversationMap.values.toList(); - list.removeWhere((element) { - if (element.conversationID == "c2c_${customerInfo?.id}") { - return true; - } + ///客服 + SocialChatUserProfile? customerInfo; + + int _systemUnreadCount() { + int count = 0; + for (final conversationId in SCGlobalConfig.systemConversationIds) { + count += conversationMap[conversationId]?.unreadCount ?? 0; + } + return count; + } + + V2TimConversation getPreferredSystemConversation() { + final systemConversations = + conversationMap.values + .where( + (element) => + SCGlobalConfig.isSystemConversationId(element.conversationID), + ) + .toList() + ..sort((e1, e2) { + final time1 = e1.lastMessage?.timestamp ?? 0; + final time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + + if (systemConversations.isNotEmpty) { + return systemConversations.first; + } + + return V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: SCGlobalConfig.primarySystemUserId, + conversationID: SCGlobalConfig.primarySystemConversationId, + ); + } + + void getConversationList() { + List list = conversationMap.values.toList(); + list.removeWhere((element) { + if (element.conversationID == "c2c_${customerInfo?.id}") { + return true; + } if (element.conversationID == "c2c_atyou-newsletter") { ///删除这个会话,后台乱发的联系人。。防止无效未读数 clearC2CHistoryMessage(element.conversationID, false); @@ -165,15 +169,15 @@ class RealTimeMessagingManager extends ChangeNotifier { } return false; }); - list.sort((e1, e2) { - int time1 = e1.lastMessage?.timestamp ?? 0; - int time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - conversationList = list; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + list.sort((e1, e2) { + int time1 = e1.lastMessage?.timestamp ?? 0; + int time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + conversationList = list; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; notifyListeners(); } @@ -234,11 +238,11 @@ class RealTimeMessagingManager extends ChangeNotifier { // _onRefreshConversation(conversationList); initConversation(); }, - onTotalUnreadMessageCountChanged: (int totalUnreadCount) { - messageUnReadCount = totalUnreadCount; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + onTotalUnreadMessageCountChanged: (int totalUnreadCount) { + messageUnReadCount = totalUnreadCount; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; allUnReadCount = messageUnReadCount + notifcationUnReadCount + @@ -478,16 +482,16 @@ class RealTimeMessagingManager extends ChangeNotifier { } } } - if (message?.userID == customerInfo?.id) { - if (onNewMessageCurrentConversationListener == null) { - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = - (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; - } - } - systemUnReadCount = _systemUnreadCount(); - notifyListeners(); - onNewMessageCurrentConversationListener?.call(message, msgId: msgId); - } + if (message?.userID == customerInfo?.id) { + if (onNewMessageCurrentConversationListener == null) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = + (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; + } + } + systemUnReadCount = _systemUnreadCount(); + notifyListeners(); + onNewMessageCurrentConversationListener?.call(message, msgId: msgId); + } void _onRefreshConversation(List conversations) { for (V2TimConversation conversation in conversations) { @@ -714,7 +718,9 @@ class RealTimeMessagingManager extends ChangeNotifier { } } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { if (entity.lengthSync() > 50000000) { - SCTts.show(SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed); + SCTts.show( + SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, + ); return; } // 复制一份视频 @@ -788,7 +794,10 @@ class RealTimeMessagingManager extends ChangeNotifier { toUser?.cleanUseProps(); toUser?.cleanPhotos(); msg.needUpDataUserInfo = - Provider.of(context!, listen: false).needUpDataUserInfo; + Provider.of( + context!, + listen: false, + ).needUpDataUserInfo; msg.user = user; msg.toUser = toUser; final textMsg = await TencentImSDKPlugin.v2TIMManager @@ -948,7 +957,8 @@ class RealTimeMessagingManager extends ChangeNotifier { ///邀请进入房间 var fdata = data["data"]; SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); - if (msg.toUserId == AccountStorage().getCurrentUser()?.userProfile?.id && + if (msg.toUserId == + AccountStorage().getCurrentUser()?.userProfile?.id && msg.roomId != Provider.of( context!, @@ -1076,11 +1086,17 @@ class RealTimeMessagingManager extends ChangeNotifier { } else { res = SCRoomThemeListRes(); } - Provider.of(context!, listen: false).updateRoomBG(res); + Provider.of( + context!, + listen: false, + ).updateRoomBG(res); return; } if (msg.type == SCRoomMsgType.emoticons) { - Provider.of(context!, listen: false).starPlayEmoji(msg); + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); return; } if (msg.type == SCRoomMsgType.micChange) { @@ -1089,7 +1105,10 @@ class RealTimeMessagingManager extends ChangeNotifier { listen: false, ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { - Provider.of(context!, listen: false).fetchOnlineUsersList(); + Provider.of( + context!, + listen: false, + ).fetchOnlineUsersList(); } else if (msg.type == SCRoomMsgType.gameLuckyGift) { var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); msg.gift = SocialChatGiftRes(giftPhoto: broadCastRes.data?.giftCover); @@ -1168,13 +1187,57 @@ class RealTimeMessagingManager extends ChangeNotifier { } } } else if (msg.type == SCRoomMsgType.gift) { + final gift = msg.gift; + final special = gift?.special ?? ""; + final giftSourceUrl = gift?.giftSourceUrl ?? ""; + final hasSource = giftSourceUrl.isNotEmpty; + final hasAnimation = scGiftHasAnimationSpecial(special); + final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name); + final hasFullScreenEffect = scGiftHasFullScreenEffect(special); + _giftFxLog( + 'recv gift msg ' + 'fromUserId=${msg.user?.id} ' + 'fromUserName=${msg.user?.userNickname} ' + 'toUserId=${msg.toUser?.id} ' + 'toUserName=${msg.toUser?.userNickname} ' + 'giftId=${gift?.id} ' + 'giftName=${gift?.giftName} ' + 'giftSourceUrl=$giftSourceUrl ' + 'special=$special ' + 'hasSource=$hasSource ' + 'hasAnimation=$hasAnimation ' + 'hasGlobalGift=$hasGlobalGift ' + 'hasFullScreenEffect=$hasFullScreenEffect ' + 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', + ); if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) { - if (msg.gift!.special!.contains(SCGiftType.ANIMSCION.name) || - msg.gift!.special!.contains(SCGiftType.GLOBAL_GIFT.name)) { + if (scGiftHasFullScreenEffect(msg.gift!.special)) { if (SCGlobalConfig.isGiftSpecialEffects) { + _giftFxLog( + 'trigger player play path=${msg.gift!.giftSourceUrl} ' + 'giftId=${msg.gift?.id} giftName=${msg.gift?.giftName}', + ); SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!); + } else { + _giftFxLog( + 'skip player play because isGiftSpecialEffects=false ' + 'giftId=${msg.gift?.id}', + ); } + } else { + _giftFxLog( + 'skip player play because special does not include ' + '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' + 'giftId=${msg.gift?.id} special=${msg.gift?.special}', + ); } + } else { + _giftFxLog( + 'skip player play because giftSourceUrl or special is null ' + 'giftId=${msg.gift?.id} ' + 'giftSourceUrl=${msg.gift?.giftSourceUrl} ' + 'special=${msg.gift?.special}', + ); } if (Provider.of( context!, @@ -1225,7 +1288,10 @@ class RealTimeMessagingManager extends ChangeNotifier { return; } else if (msg.type == SCRoomMsgType.roomRoleChange) { ///房间身份变动 - Provider.of(context!, listen: false).retrieveMicrophoneList(); + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(); if (msg.toUser?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { Provider.of( @@ -1389,13 +1455,13 @@ class RealTimeMessagingManager extends ChangeNotifier { notifyListeners(); } - void updateSystemCount(int count) { - for (final conversationId in SCGlobalConfig.systemConversationIds) { - conversationMap[conversationId]?.unreadCount = 0; - } - systemUnReadCount = 0; - notifyListeners(); - } + void updateSystemCount(int count) { + for (final conversationId in SCGlobalConfig.systemConversationIds) { + conversationMap[conversationId]?.unreadCount = 0; + } + systemUnReadCount = 0; + notifyListeners(); + } void updateCustomerCount(int count) { conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index cd02a7a..cc7f15a 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -1,272 +1,258 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svga/flutter_svga.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; -import 'package:yumi/shared/data_sources/models/country_mode.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; -import 'package:yumi/shared/business_logic/models/res/country_res.dart'; -import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; - -import '../../shared/data_sources/models/enum/sc_banner_type.dart'; - -class SCAppGeneralManager extends ChangeNotifier { - List countryModeList = []; - Map> _countryMap = {}; - Map _countryByNameMap = {}; - bool _isFetchingCountryList = false; - - ///礼物 - List giftResList = []; - Map _giftByIdMap = {}; - - Map> giftByTab = {}; - Map> activityGiftByTab = {}; - - ///选中的国家 - Country? selectCountryInfo; - - List activityList = []; - - SCAppGeneralManager() { - fetchCountryList(); - giftList(); // 使用默认参数 - giftActivityList(); - // giftBackpack(); - // configLevel(); - } - - ///加载国家列表 - Future fetchCountryList() async { - if (_isFetchingCountryList) { - return; - } - if (countryModeList.isNotEmpty) { - clearCountrySelection(); - notifyListeners(); - return; - } - - _isFetchingCountryList = true; - try { - await Future.delayed(Duration(milliseconds: 550)); - final result = await SCConfigRepositoryImp().loadCountry(); - _countryMap.clear(); - _countryByNameMap.clear(); - countryModeList.clear(); - - for (final c in result.openCountry ?? const []) { - final prefix = c.aliasName?.substring(0, 1) ?? ""; - if (prefix.isNotEmpty) { - final countryList = _countryMap[prefix] ?? []; - countryList.add(c.copyWith()); - _countryMap[prefix] = countryList; - } - if ((c.countryName ?? "").isNotEmpty) { - _countryByNameMap[c.countryName!] = c; - } - } - - _countryMap.forEach((k, v) { - countryModeList.add(CountryMode(k, v)); - }); - - ///排序 - countryModeList.sort((a, b) => a.prefix.compareTo(b.prefix)); - notifyListeners(); - } catch (_) { - notifyListeners(); - } finally { - _isFetchingCountryList = false; - } - } - - ///国家国家名称获取国家 - Country? findCountryByName(String countryName) { - return _countryByNameMap[countryName]; - } - - ///选择国家 - Country? chooseCountry(int subIndex, int cIndex) { - var selectCm = countryModeList[subIndex].prefixCountrys[cIndex]; - if (selectCountryInfo != null) { - selectCountryInfo = null; - } else { - selectCountryInfo = selectCm; - } - return selectCountryInfo; - } - - void updateCurrentCountry(Country? countryInfo) { - selectCountryInfo = countryInfo; - notifyListeners(); - } - - void clearCountrySelection() { - selectCountryInfo = null; - } - - void openDrawer(BuildContext context) { - Scaffold.of(context).openDrawer(); - } - - void closeDrawer(BuildContext context) { - Scaffold.of(context).closeDrawer(); - } - - void giftList({bool includeCustomized = true}) async { - try { - giftResList = await SCChatRoomRepository().giftList(); - giftByTab.clear(); - for (var gift in giftResList) { - giftByTab[gift.giftTab]; - var gmap = giftByTab[gift.giftTab]; - gmap ??= []; - gmap.add(gift); - giftByTab[gift.giftTab!] = gmap; - var gAllMap = giftByTab["ALL"]; - gAllMap ??= []; - if (gift.giftTab != "NSCIONAL_FLAG" && - gift.giftTab != "ACTIVITY" && - gift.giftTab != "LUCKY_GIFT" && - gift.giftTab != "CP" && - gift.giftTab != "CUSTOMIZED" && - gift.giftTab != "MAGIC") { - gAllMap.add(gift); - } - giftByTab["ALL"] = gAllMap; - _giftByIdMap[gift.id!] = gift; - } - - if (includeCustomized) { - giftByTab["CUSTOMIZED"] ??= []; - - ///定制礼物需要一个占位符,显示规则的 - giftByTab["CUSTOMIZED"]?.insert(0, SocialChatGiftRes(id: "-1000")); - } else { - giftByTab.remove("CUSTOMIZED"); - } - - notifyListeners(); - - ///先去预下载 - downLoad(giftResList); - } catch (e) {} - } - - void giftActivityList() async { - try { - activityList.clear(); - activityList = await SCChatRoomRepository().giftActivityList(); - activityGiftByTab.clear(); - for (var gift in activityList) { - var gmap = activityGiftByTab["${gift.activityId}"]; - gmap ??= []; - gmap.add(gift); - activityGiftByTab["${gift.activityId}"] = gmap; - } - notifyListeners(); - } catch (e) {} - } - - ///礼物背包 - void giftBackpack() async { - try { - var result = await SCChatRoomRepository().giftBackpack(); - } catch (e) {} - } - - ///礼物id获取礼物 - SocialChatGiftRes? getGiftById(String id) { - return _giftByIdMap[id]; - } - - void downLoad(List giftResList) { - for (var gift in giftResList) { - if (gift.giftSourceUrl != null) { - if (SCPathUtils.getFileExtension(gift.giftSourceUrl!).toLowerCase() == - ".svga") { - ///预解析 - if (!SCGiftVapSvgaManager().videoItemCache.containsKey( - gift.giftSourceUrl, - )) { - SVGAParser.shared.decodeFromURL(gift.giftSourceUrl!).then((entity) { - entity.autorelease = false; - SCGiftVapSvgaManager().videoItemCache[gift.giftSourceUrl!] = - entity; - }); - } - } else { - if ((gift.giftSourceUrl ?? "").isNotEmpty) { - FileCacheManager.getInstance().getFile(url: gift.giftSourceUrl!); - } - } - } - } - } - - List homeBanners = []; - List exploreBanners = []; - List roomBanners = []; - List gameBanners = []; - - ///首页banner - void loadMainBanner() async { - try { - var banners = await SCConfigRepositoryImp().getBanner(); - homeBanners.clear(); - roomBanners.clear(); - gameBanners.clear(); - exploreBanners.clear(); - for (var v in banners) { - if ((v.displayPosition ?? "").contains( - SCBannerType.EXPLORE_PAGE.name, - )) { - exploreBanners.add(v); - } - if ((v.displayPosition ?? "").contains(SCBannerType.ROOM.name)) { - roomBanners.add(v); - } - - if ((v.displayPosition ?? "").contains(SCBannerType.HOME_ALERT.name)) { - homeBanners.add(v); - } - if ((v.displayPosition ?? "").contains(SCBannerType.GAME.name)) { - gameBanners.add(v); - } - } - notifyListeners(); - } catch (e) {} - } - - Map> emojiByTab = {}; - - ///emoji表情 - void emojiAll() async { - var roomEmojis = await SCChatRoomRepository().emojiAll(); - for (var value in roomEmojis) { - emojiByTab[value.groupName!] = value.emojis ?? []; - } - notifyListeners(); - } - - ///加载等级资源 - void configLevel() { - SCConfigRepositoryImp().configLevel(); - } - - SCBannerLeaderboardRes? appLeaderResult; - - ///查询榜单前三名 - void appLeaderboard() async { - try { - appLeaderResult = await SCChatRoomRepository().appLeaderboard(); - notifyListeners(); - } catch (e) {} - } -} +import 'package:flutter/material.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/models/country_mode.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; + +import '../../shared/data_sources/models/enum/sc_banner_type.dart'; +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; + +class SCAppGeneralManager extends ChangeNotifier { + List countryModeList = []; + Map> _countryMap = {}; + Map _countryByNameMap = {}; + bool _isFetchingCountryList = false; + + ///礼物 + List giftResList = []; + Map _giftByIdMap = {}; + + Map> giftByTab = {}; + Map> activityGiftByTab = {}; + + ///选中的国家 + Country? selectCountryInfo; + + List activityList = []; + + SCAppGeneralManager() { + fetchCountryList(); + giftList(); // 使用默认参数 + giftActivityList(); + // giftBackpack(); + // configLevel(); + } + + ///加载国家列表 + Future fetchCountryList() async { + if (_isFetchingCountryList) { + return; + } + if (countryModeList.isNotEmpty) { + clearCountrySelection(); + notifyListeners(); + return; + } + + _isFetchingCountryList = true; + try { + await Future.delayed(Duration(milliseconds: 550)); + final result = await SCConfigRepositoryImp().loadCountry(); + _countryMap.clear(); + _countryByNameMap.clear(); + countryModeList.clear(); + + for (final c in result.openCountry ?? const []) { + final prefix = c.aliasName?.substring(0, 1) ?? ""; + if (prefix.isNotEmpty) { + final countryList = _countryMap[prefix] ?? []; + countryList.add(c.copyWith()); + _countryMap[prefix] = countryList; + } + if ((c.countryName ?? "").isNotEmpty) { + _countryByNameMap[c.countryName!] = c; + } + } + + _countryMap.forEach((k, v) { + countryModeList.add(CountryMode(k, v)); + }); + + ///排序 + countryModeList.sort((a, b) => a.prefix.compareTo(b.prefix)); + notifyListeners(); + } catch (_) { + notifyListeners(); + } finally { + _isFetchingCountryList = false; + } + } + + ///国家国家名称获取国家 + Country? findCountryByName(String countryName) { + return _countryByNameMap[countryName]; + } + + ///选择国家 + Country? chooseCountry(int subIndex, int cIndex) { + var selectCm = countryModeList[subIndex].prefixCountrys[cIndex]; + if (selectCountryInfo != null) { + selectCountryInfo = null; + } else { + selectCountryInfo = selectCm; + } + return selectCountryInfo; + } + + void updateCurrentCountry(Country? countryInfo) { + selectCountryInfo = countryInfo; + notifyListeners(); + } + + void clearCountrySelection() { + selectCountryInfo = null; + } + + void openDrawer(BuildContext context) { + Scaffold.of(context).openDrawer(); + } + + void closeDrawer(BuildContext context) { + Scaffold.of(context).closeDrawer(); + } + + void giftList({bool includeCustomized = true}) async { + try { + giftResList = await SCChatRoomRepository().giftList(); + giftByTab.clear(); + for (var gift in giftResList) { + giftByTab[gift.giftTab]; + var gmap = giftByTab[gift.giftTab]; + gmap ??= []; + gmap.add(gift); + giftByTab[gift.giftTab!] = gmap; + var gAllMap = giftByTab["ALL"]; + gAllMap ??= []; + if (gift.giftTab != "NSCIONAL_FLAG" && + gift.giftTab != "ACTIVITY" && + gift.giftTab != "LUCKY_GIFT" && + gift.giftTab != "CP" && + gift.giftTab != "CUSTOMIZED" && + gift.giftTab != "MAGIC") { + gAllMap.add(gift); + } + giftByTab["ALL"] = gAllMap; + _giftByIdMap[gift.id!] = gift; + } + + if (includeCustomized) { + giftByTab["CUSTOMIZED"] ??= []; + + ///定制礼物需要一个占位符,显示规则的 + giftByTab["CUSTOMIZED"]?.insert(0, SocialChatGiftRes(id: "-1000")); + } else { + giftByTab.remove("CUSTOMIZED"); + } + + notifyListeners(); + + ///先去预下载 + downLoad(giftResList); + } catch (e) {} + } + + void giftActivityList() async { + try { + activityList.clear(); + activityList = await SCChatRoomRepository().giftActivityList(); + activityGiftByTab.clear(); + for (var gift in activityList) { + var gmap = activityGiftByTab["${gift.activityId}"]; + gmap ??= []; + gmap.add(gift); + activityGiftByTab["${gift.activityId}"] = gmap; + } + notifyListeners(); + } catch (e) {} + } + + ///礼物背包 + void giftBackpack() async { + try { + var result = await SCChatRoomRepository().giftBackpack(); + } catch (e) {} + } + + ///礼物id获取礼物 + SocialChatGiftRes? getGiftById(String id) { + return _giftByIdMap[id]; + } + + void downLoad(List giftResList) { + for (var gift in giftResList) { + final giftSourceUrl = gift.giftSourceUrl ?? ""; + if (giftSourceUrl.isEmpty) { + continue; + } + if (!scGiftHasFullScreenEffect(gift.special)) { + continue; + } + SCGiftVapSvgaManager().preload(giftSourceUrl); + } + } + + List homeBanners = []; + List exploreBanners = []; + List roomBanners = []; + List gameBanners = []; + + ///首页banner + void loadMainBanner() async { + try { + var banners = await SCConfigRepositoryImp().getBanner(); + homeBanners.clear(); + roomBanners.clear(); + gameBanners.clear(); + exploreBanners.clear(); + for (var v in banners) { + if ((v.displayPosition ?? "").contains( + SCBannerType.EXPLORE_PAGE.name, + )) { + exploreBanners.add(v); + } + if ((v.displayPosition ?? "").contains(SCBannerType.ROOM.name)) { + roomBanners.add(v); + } + + if ((v.displayPosition ?? "").contains(SCBannerType.HOME_ALERT.name)) { + homeBanners.add(v); + } + if ((v.displayPosition ?? "").contains(SCBannerType.GAME.name)) { + gameBanners.add(v); + } + } + notifyListeners(); + } catch (e) {} + } + + Map> emojiByTab = {}; + + ///emoji表情 + void emojiAll() async { + var roomEmojis = await SCChatRoomRepository().emojiAll(); + for (var value in roomEmojis) { + emojiByTab[value.groupName!] = value.emojis ?? []; + } + notifyListeners(); + } + + ///加载等级资源 + void configLevel() { + SCConfigRepositoryImp().configLevel(); + } + + SCBannerLeaderboardRes? appLeaderResult; + + ///查询榜单前三名 + void appLeaderboard() async { + try { + appLeaderResult = await SCChatRoomRepository().appLeaderboard(); + notifyListeners(); + } catch (e) {} + } +} diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart index 34aadc3..48bb10c 100644 --- a/lib/services/gift/gift_animation_manager.dart +++ b/lib/services/gift/gift_animation_manager.dart @@ -11,6 +11,9 @@ class GiftAnimationManager extends ChangeNotifier { //每个控件正在播放的动画 Map giftMap = {0: null, 1: null, 2: null, 3: null}; + bool get _controllersReady => + animationControllerList.length >= giftMap.length; + void enqueueGiftAnimation(LGiftModel giftModel) { pendingAnimationsQueue.add(giftModel); proceedToNextAnimation(); @@ -18,7 +21,7 @@ class GiftAnimationManager extends ChangeNotifier { ///开始播放 proceedToNextAnimation() { - if (pendingAnimationsQueue.isEmpty) { + if (pendingAnimationsQueue.isEmpty || !_controllersReady) { return; } var playGift = pendingAnimationsQueue.first; @@ -45,6 +48,7 @@ class GiftAnimationManager extends ChangeNotifier { void attachAnimationControllers(List anins) { animationControllerList = anins; + proceedToNextAnimation(); } void cleanupAnimationResources() { diff --git a/lib/services/room/rc_room_manager.dart b/lib/services/room/rc_room_manager.dart index 1765129..f832211 100644 --- a/lib/services/room/rc_room_manager.dart +++ b/lib/services/room/rc_room_manager.dart @@ -21,7 +21,7 @@ class SocialChatRoomManager extends ChangeNotifier { void updateMyRoomInfo(SCEditRoomInfoRes roomInfo) { if (myRoom != null && myRoom!.id == roomInfo.id) { - if (roomInfo.roomCover != null) { + if ((roomInfo.roomCover ?? "").trim().isNotEmpty) { myRoom?.setRoomCover = roomInfo.roomCover!; } if (roomInfo.roomName != null) { diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index abe512e..fa628e4 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -1,6 +1,7 @@ import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import '../../../data_sources/models/enum/sc_props_type.dart'; + /// token : "" /// userCredential : {"expireTime":0,"releaseTime":0,"sign":"","sysOrigin":"","userId":0,"version":""} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} @@ -214,7 +215,7 @@ class SocialChatUserProfile { _useProps?.add(UseProps.fromJson(v)); }); } - _userAvatar = json['userAvatar']; + _userAvatar = json['userAvatar'] ?? json['avatar']; _roles = json['roles']; _userNickname = json['userNickname']; _hobby = json['hobby']; @@ -462,6 +463,7 @@ class SocialChatUserProfile { void cleanWearBadge() { _wearBadge?.clear(); } + void cleanUseProps() { _useProps?.clear(); } diff --git a/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart index 2afc832..b1ea990 100644 --- a/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart +++ b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart @@ -53,7 +53,7 @@ class SCEditRoomInfoRes { } SCEditRoomInfoRes.fromJson(dynamic json) { - _id = json['id']; + _id = json['id'] ?? json['roomId']; _roomAccount = json['roomAccount']; _userId = json['userId']; _roomCover = json['roomCover'] ?? json['cover']; diff --git a/lib/shared/data_sources/models/enum/sc_gift_type.dart b/lib/shared/data_sources/models/enum/sc_gift_type.dart index 14a373a..2b6b16a 100644 --- a/lib/shared/data_sources/models/enum/sc_gift_type.dart +++ b/lib/shared/data_sources/models/enum/sc_gift_type.dart @@ -1,8 +1,15 @@ -enum SCGiftType{ - ANIMSCION, - MUSIC, - GLOBAL_GIFT, - LUCKY_GIFT, - MAGIC, - CP, -} \ No newline at end of file +enum SCGiftType { ANIMSCION, MUSIC, GLOBAL_GIFT, LUCKY_GIFT, MAGIC, CP } + +const String kSCGiftAnimationSpecialAlias = 'ANIMATION'; + +bool scGiftHasAnimationSpecial(String? special) { + final value = special ?? ''; + return value.contains(SCGiftType.ANIMSCION.name) || + value.contains(kSCGiftAnimationSpecialAlias); +} + +bool scGiftHasFullScreenEffect(String? special) { + final value = special ?? ''; + return scGiftHasAnimationSpecial(value) || + value.contains(SCGiftType.GLOBAL_GIFT.name); +} diff --git a/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart index aa152f3..42ab55d 100644 --- a/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart @@ -1,26 +1,29 @@ -import 'dart:io'; -import 'package:yumi/shared/business_logic/repositories/general_repository.dart'; -import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; -import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; - -class SCGeneralRepositoryImp implements SocialChatGeneralRepository { - static SCGeneralRepositoryImp? _instance; - - SCGeneralRepositoryImp._internal(); - - factory SCGeneralRepositoryImp() { - return _instance ??= SCGeneralRepositoryImp._internal(); - } - +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:yumi/shared/business_logic/repositories/general_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCGeneralRepositoryImp implements SocialChatGeneralRepository { + static SCGeneralRepositoryImp? _instance; + + SCGeneralRepositoryImp._internal(); + + factory SCGeneralRepositoryImp() { + return _instance ??= SCGeneralRepositoryImp._internal(); + } + ///external/oss/upload @override Future upload(File image) async { String filePath = image.path; - var name = - filePath.substring(filePath.lastIndexOf("/") + 1, filePath.length); - FormData formData = FormData.fromMap( - {"file": await MultipartFile.fromFile(filePath, filename: name)}, + var name = filePath.substring( + filePath.lastIndexOf("/") + 1, + filePath.length, ); + FormData formData = FormData.fromMap({ + "file": await MultipartFile.fromFile(filePath, filename: name), + }); final response = await http.dio.post( "91458261d1ab337ad7b1da3610dd496cf7f58dbb0fecacfbdfe516c4892b1694", data: formData, @@ -28,15 +31,15 @@ class SCGeneralRepositoryImp implements SocialChatGeneralRepository { contentType: 'multipart/form-data', sendTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60), - extra: { - TimeOutInterceptor.timeoutSecondsKey: 60, - }, + extra: {TimeOutInterceptor.timeoutSecondsKey: 60}, ), ); final baseResponse = ResponseData.fromJson( response.data as Map, fromJsonT: (json) => json as String, ); + debugPrint("[OSS Upload] response.data: ${response.data}"); + debugPrint("[OSS Upload] parsed file url: ${baseResponse.body ?? ""}"); return baseResponse.body ?? ""; } } diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index 8810bf9..e70d4fe 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -34,6 +34,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.da import 'package:yumi/shared/business_logic/repositories/room_repository.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; +import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; class SCChatRoomRepository implements SocialChatRoomRepository { static SCChatRoomRepository? _instance; @@ -55,9 +56,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository { "363603111e51beac2d183014dd29b81ced652239ec13178acd7001096cab7429", queryParams: queryParams, fromJson: - (json) => (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), + (json) => + (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), ); - return result; + return result.map(SCRoomProfileCache.applyToSocialChatRoomRes).toList(); } ///live/mic/list @@ -84,7 +86,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { queryParams: queryParams, fromJson: (json) => MyRoomRes.fromJson(json), ); - return result; + return SCRoomProfileCache.applyToMyRoomRes(result); } ///room/relation/status @@ -115,7 +117,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository { ///activity/room-contribution-activity @override - Future roomContributionActivity(String roomId) async { + Future roomContributionActivity( + String roomId, + ) async { Map queryParams = {}; queryParams["roomId"] = roomId; final result = await http.get( @@ -191,7 +195,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository { "a01ade8fbb604c0904557a9483fcc4d9", queryParams: {"roomId": roomId}, fromJson: - (json) => (json as List).map((e) => SocialChatUserProfile.fromJson(e)).toList(), + (json) => + (json as List) + .map((e) => SocialChatUserProfile.fromJson(e)) + .toList(), ); return result; } @@ -213,7 +220,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.get>( "3a613b7450f8fd9082b988bd21df454d", fromJson: - (json) => (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), + (json) => + (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), ); return result; } @@ -317,9 +325,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository { "363603111e51beac2d183014dd29b81cce9dda0f9cd8c267f6e4fd3f05bda090", queryParams: {"roomAccount": roomAccount}, fromJson: - (json) => (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), + (json) => + (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), ); - return result; + return result.map(SCRoomProfileCache.applyToSocialChatRoomRes).toList(); } ///room/blacklist/join @@ -500,7 +509,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository { queryParams: parm, fromJson: (json) => - (json as List).map((e) => SocialChatRoomMemberRes.fromJson(e)).toList(), + (json as List) + .map((e) => SocialChatRoomMemberRes.fromJson(e)) + .toList(), ); return result; } @@ -584,7 +595,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository { "48ab0afa0e4305070eaa82edc3fc3996", fromJson: (json) => - (json as List).map((e) => SocialChatGiftBackpackRes.fromJson(e)).toList(), + (json as List) + .map((e) => SocialChatGiftBackpackRes.fromJson(e)) + .toList(), ); return result; } @@ -775,7 +788,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.get>( "60296415428765e04263c733bfc8428c397bb180e31856d94bf25d85eb342787", fromJson: - (json) => (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), + (json) => + (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), ); return result; } @@ -815,11 +829,4 @@ class SCChatRoomRepository implements SocialChatRoomRepository { ); return result; } - - - - - - - } diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 9ee1684..88f16cf 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -3,7 +3,8 @@ import 'package:yumi/shared/business_logic/models/res/sc_gold_record_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; -import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/room_res.dart' + hide WearBadge; import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; @@ -13,9 +14,12 @@ import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_re import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; import 'package:yumi/shared/business_logic/models/req/sc_mobile_auth_cmd.dart'; import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart'; -import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' hide WearBadge; -import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart' hide WearBadge; -import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' + hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart' + hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' + hide WearBadge; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart'; import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; @@ -24,6 +28,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; import 'package:yumi/shared/business_logic/repositories/user_repository.dart'; import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; +import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; class SCAccountRepository implements SocialChatUserRepository { static SCAccountRepository? _instance; @@ -83,7 +88,10 @@ class SCAccountRepository implements SocialChatUserRepository { ///auth/account/login/channel @override - Future loginForChannel(String authType, String openId) async { + Future loginForChannel( + String authType, + String openId, + ) async { final result = await http.post( "e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44", data: {"authType": authType, "openId": openId}, @@ -106,7 +114,7 @@ class SCAccountRepository implements SocialChatUserRepository { (json) => (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), ); - return result; + return result.map(SCRoomProfileCache.applyToFollowRoomRes).toList(); } ///room/relation/joined @@ -118,7 +126,7 @@ class SCAccountRepository implements SocialChatUserRepository { (json) => (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), ); - return result; + return result.map(SCRoomProfileCache.applyToFollowRoomRes).toList(); } ///room/relation/trace @@ -135,7 +143,7 @@ class SCAccountRepository implements SocialChatUserRepository { (json) => (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), ); - return result; + return result.map(SCRoomProfileCache.applyToFollowRoomRes).toList(); } ///room/profile @@ -145,7 +153,7 @@ class SCAccountRepository implements SocialChatUserRepository { "1e41384e55cbd6c2374608b129e2ed27", fromJson: (json) => MyRoomRes.fromJson(json), ); - return result; + return SCRoomProfileCache.applyToMyRoomRes(result); } ///room/live-voice/create @@ -179,7 +187,7 @@ class SCAccountRepository implements SocialChatUserRepository { data: param, fromJson: (json) => JoinRoomRes.fromJson(json), ); - return result; + return SCRoomProfileCache.applyToJoinRoomRes(result); } ///live/user/heartbeat @@ -235,13 +243,21 @@ class SCAccountRepository implements SocialChatUserRepository { "1e41384e55cbd6c2374608b129e2ed27", data: { "id": roomId, + "roomId": roomId, "roomCover": roomCover, + "cover": roomCover, "roomName": roomName, "roomDesc": roomDesc, "event": event, }, fromJson: (json) => SCEditRoomInfoRes.fromJson(json), ); + await SCRoomProfileCache.saveRoomProfile( + roomId: roomId, + roomCover: roomCover, + roomName: roomName, + roomDesc: roomDesc, + ); return result; } @@ -431,10 +447,10 @@ class SCAccountRepository implements SocialChatUserRepository { parm["personalPhotos"] = personalPhotos; } - final result = await http.put( - "2fa1d4f56bb726558904ce2a50b83f96365e62b6aa808acde7b56d2d5945fbe4", - data: parm, - fromJson: (json) => SocialChatUserProfile.fromJson(json), + final result = await http.put( + "2fa1d4f56bb726558904ce2a50b83f96365e62b6aa808acde7b56d2d5945fbe4", + data: parm, + fromJson: (json) => SocialChatUserProfile.fromJson(json), ); return result; } @@ -535,7 +551,6 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///user/user-level/consumption/exp @override Future userLevelConsumptionExp( @@ -757,7 +772,8 @@ class SCAccountRepository implements SocialChatUserRepository { final result = await http.get>( "6e87048da643aa0388a9e46ea391daff574aff257ce7e668d08f4caccd1c6232", fromJson: - (json) => (json as List).map((e) => SCTaskListRes.fromJson(e)).toList(), + (json) => + (json as List).map((e) => SCTaskListRes.fromJson(e)).toList(), ); return result; } @@ -915,7 +931,6 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///user/cp-relationship/create-apply @override Future cpRelationshipSendApply(String acceptApplyUserId) async { @@ -1086,7 +1101,6 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///user/vip/ability/update @override Future userVipAbilityUpdate({ diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 1acb053..9447bad 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -1,305 +1,567 @@ -import 'package:flutter/animation.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_svga/flutter_svga.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:tancent_vap/utils/constant.dart'; -import 'package:tancent_vap/widgets/vap_view.dart'; - -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; - -class SCGiftVapSvgaManager { - Map videoItemCache = {}; - static SCGiftVapSvgaManager? _inst; - - SCGiftVapSvgaManager._internal(); - - factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); - - final SCPriorityQueue _tq = SCPriorityQueue( - (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 - ); - VapController? _rgc; - SVGAAnimationController? _rsc; - bool _play = false; - bool _dis = false; - - bool _pause = false; - - //是否关闭礼物特效声音 - bool _mute = false; - - void setMute(bool muteMusic) { - _mute = muteMusic; - DataPersistence.setPlayGiftMusic(_mute); - } - - bool getMute() { - return _mute; - } - - // 绑定控制器 - void bindVapCtrl(VapController vapController) { - _mute = DataPersistence.getPlayGiftMusic(); - _dis = false; - _rgc = vapController; - _rgc?.setAnimListener( - onVideoStart: () { - }, - onVideoComplete: () { - _hcs(); - }, - onFailed: (code, type, msg) { - _hcs(); - }, - ); - } - - void bindSvgaCtrl(SVGAAnimationController svgaController) { - _dis = false; - _rsc = svgaController; - _rsc?.addStatusListener((AnimationStatus status) { - if (status.isCompleted) { - _rsc?.reset(); - _play = false; - _pn(); - } - }); - } - - // 播放任务 - void play( - String path, { - int priority = 0, - Map? customResources, - int type = 0, - }) { - if (path.isEmpty) { - return; - } - if (SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim) { - if (_dis) return; - final task = SCVapTask( - path: path, - priority: priority, - customResources: customResources, - ); - - _tq.add(task); - if (!_play) { - _pn(); - } - } - } - - // 播放下一个任务 - Future _pn() async { - if (_pause) { - return; - } - if (_dis || _tq.isEmpty || _play) return; - - final task = _tq.removeFirst(); - _play = true; - try { - final pathType = SCPathUtils.getPathType(task.path); - if (pathType == PathType.asset) { - await _pa(task); - } else if (pathType == PathType.file) { - await _pf(task); - } else if (pathType == PathType.network) { - await _pnw(task); - } - } catch (e, s) { - print('VAP_SVGA播放失败: $e\n$s'); - } finally { - // 确保状态正确重置 - // if (!_dis) { - // _pn(); - // } - } - } - - // 播放资源文件 - Future _pa(SCVapTask task) async { - if (_dis) return; - if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { - MovieEntity entity; - if (videoItemCache.containsKey(task.path)) { - entity = videoItemCache[task.path]!; - } else { - entity = await SVGAParser.shared.decodeFromAssets(task.path); - videoItemCache[task.path] = entity; - } - _rsc?.videoItem = entity; - _rsc?.reset(); - _rsc?.forward(); - } else { - if (task.customResources != null) { - task.customResources?.forEach((k, v) async { - await _rgc?.setVapTagContent(k, v); - }); - } - await _rgc?.playAsset(task.path); - } - } - - // 播放本地文件 - Future _pf(SCVapTask task) async { - if (_dis || _rgc == null) return; - await _rgc?.setMute(_mute); - if (task.customResources != null) { - task.customResources?.forEach((k, v) async { - await _rgc?.setVapTagContent(k, v); - }); - } - await _rgc!.playFile(task.path); - } - - // 播放网络资源 - Future _pnw(SCVapTask task) async { - if (_dis) return; - if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { - MovieEntity? entity; - if (videoItemCache.containsKey(task.path)) { - entity = videoItemCache[task.path]!; - entity.autorelease = false; - } else { - try { - entity = await SVGAParser.shared.decodeFromURL(task.path); - entity.autorelease = false; - videoItemCache[task.path] = entity; - } catch (e) { - _play = false; - print('svga解析出错:$e'); - } - } - if (entity != null) { - _rsc?.videoItem = entity; - _rsc?.reset(); - _rsc?.forward(); - } - } else { - final file = await FileCacheManager.getInstance().getFile(url: task.path); - if (file != null && !_dis) { - await _pf( - SCVapTask( - path: file.path, - priority: task.priority, - customResources: task.customResources, - ), - ); - } - } - } - - // 处理控制器状态变化 - void _hcs() { - if (_rgc != null && !_dis) { - _play = false; - // 延迟一小段时间后播放下一个,避免状态冲突 - Future.delayed(const Duration(milliseconds: 50), _pn); - } - } - - //暂停动画播放 - void pauseAnim() { - _pause = true; - } - - //恢复动画播放 - void resumeAnim() { - _pause = false; - _pn(); - } - - // 释放资源 - void dispose() { - _dis = true; - _play = false; - _tq.clear(); - _rgc?.stop(); - _rgc?.dispose(); - _rgc = null; - _rsc?.stop(); - _rsc?.dispose(); - _rsc = null; - } - - // 清除所有任务 - void clearTasks() { - _play = false; - _tq.clear(); - _pause = false; - } -} - -// 任务模型 -// 1. 修改 SCVapTask 类,实现 Comparable - -class SCVapTask implements Comparable { - final String path; - final int type; - final int priority; - final Map? customResources; - final int _seq; - - static int _nextSeq = 0; - - SCVapTask({ - required this.path, - this.priority = 0, - this.customResources, - this.type = 0, - }) : _seq = _nextSeq++; - - @override - int compareTo(SCVapTask other) { - // 先按优先级降序排列 - int priorityComparison = other.priority.compareTo(priority); - if (priorityComparison != 0) { - return priorityComparison; - } - // 相同优先级时,按序列号升序排列(先添加的先执行) - return _seq.compareTo(other._seq); - } - - @override - String toString() { - return 'SCVapTask{path: $path, priority: $priority, seq: $_seq}'; - } -} - -// 优先队列实现 -class SCPriorityQueue { - final List _els = []; - final Comparator _cmp; - - SCPriorityQueue(this._cmp); - - void add(E element) { - _els.add(element); - _els.sort(_cmp); - } - - E removeFirst() { - if (isEmpty) throw StateError("No elements"); - return _els.removeAt(0); - } - - E? get first => isEmpty ? null : _els.first; - - bool get isEmpty => _els.isEmpty; - - bool get isNotEmpty => _els.isNotEmpty; - - int get length => _els.length; - - void clear() => _els.clear(); - - List get unorderedElements => List.from(_els); - - // 实现 Iterable 接口 - Iterator get iterator => _els.iterator; -} +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; + +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; + +class SCGiftVapSvgaManager { + Map videoItemCache = {}; + static SCGiftVapSvgaManager? _inst; + static const int _maxPreloadConcurrency = 1; + + SCGiftVapSvgaManager._internal(); + + factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); + + final SCPriorityQueue _tq = SCPriorityQueue( + (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 + ); + VapController? _rgc; + SVGAAnimationController? _rsc; + bool _play = false; + bool _dis = false; + SCVapTask? _currentTask; + final Queue _preloadQueue = Queue(); + final Set _queuedPreloadPaths = {}; + final Map> _svgaLoadTasks = {}; + final Map> _playablePathTasks = {}; + final Map _playablePathCache = {}; + int _activePreloadCount = 0; + + bool _pause = false; + + //是否关闭礼物特效声音 + bool _mute = false; + + void setMute(bool muteMusic) { + _mute = muteMusic; + DataPersistence.setPlayGiftMusic(_mute); + } + + bool getMute() { + return _mute; + } + + void _log(String message) { + debugPrint('[GiftFX][Player] $message'); + } + + bool _needsSvgaController(String path) { + return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; + } + + bool _isControllerReady(SCVapTask task) { + if (_needsSvgaController(task.path)) { + return _rsc != null; + } + return _rgc != null; + } + + bool _isPlayableFileReady(String path) { + final cachedPath = _playablePathCache[path]; + if (cachedPath == null || cachedPath.isEmpty) { + return false; + } + return File(cachedPath).existsSync(); + } + + bool _isPreloadedOrLoading(String path) { + if (_needsSvgaController(path)) { + return videoItemCache.containsKey(path) || + _svgaLoadTasks.containsKey(path); + } + final pathType = SCPathUtils.getPathType(path); + if (pathType == PathType.asset || pathType == PathType.file) { + return true; + } + return _isPlayableFileReady(path) || _playablePathTasks.containsKey(path); + } + + Future preload(String path, {bool highPriority = false}) async { + if (path.isEmpty || _dis || _isPreloadedOrLoading(path)) { + return; + } + if (highPriority) { + _log('high priority preload path=$path'); + await _warmupPath(path); + return; + } + if (_queuedPreloadPaths.contains(path)) { + return; + } + _preloadQueue.add(path); + _queuedPreloadPaths.add(path); + _log('enqueue preload path=$path queue=${_preloadQueue.length}'); + _drainPreloadQueue(); + } + + void _drainPreloadQueue() { + if (_dis || + _pause || + _play || + _activePreloadCount >= _maxPreloadConcurrency || + _preloadQueue.isEmpty) { + return; + } + final path = _preloadQueue.removeFirst(); + _queuedPreloadPaths.remove(path); + _activePreloadCount++; + _log( + 'start preload path=$path active=$_activePreloadCount ' + 'queueRemaining=${_preloadQueue.length}', + ); + _warmupPath(path).whenComplete(() { + _activePreloadCount--; + _log( + 'finish preload path=$path active=$_activePreloadCount ' + 'queueRemaining=${_preloadQueue.length}', + ); + _drainPreloadQueue(); + }); + } + + Future _warmupPath(String path) async { + if (_needsSvgaController(path)) { + await _loadSvgaEntity(path); + return; + } + await _ensurePlayableFilePath(path); + } + + Future _loadSvgaEntity(String path) async { + final cached = videoItemCache[path]; + if (cached != null) { + return cached; + } + final loadingTask = _svgaLoadTasks[path]; + if (loadingTask != null) { + return loadingTask; + } + final future = () async { + final pathType = SCPathUtils.getPathType(path); + late final MovieEntity entity; + if (pathType == PathType.asset) { + entity = await SVGAParser.shared.decodeFromAssets(path); + } else if (pathType == PathType.network) { + entity = await SVGAParser.shared.decodeFromURL(path); + } else if (pathType == PathType.file) { + final bytes = await File(path).readAsBytes(); + entity = await SVGAParser.shared.decodeFromBuffer(bytes); + } else { + throw Exception('Unsupported SVGA path: $path'); + } + entity.autorelease = false; + videoItemCache[path] = entity; + return entity; + }(); + _svgaLoadTasks[path] = future; + try { + return await future; + } finally { + _svgaLoadTasks.remove(path); + } + } + + Future _ensurePlayableFilePath(String path) async { + final pathType = SCPathUtils.getPathType(path); + if (pathType == PathType.asset || pathType == PathType.file) { + return path; + } + final cachedPath = _playablePathCache[path]; + if (cachedPath != null && + cachedPath.isNotEmpty && + File(cachedPath).existsSync()) { + return cachedPath; + } + final loadingTask = _playablePathTasks[path]; + if (loadingTask != null) { + return loadingTask; + } + final future = () async { + final file = await FileCacheManager.getInstance().getFile(url: path); + _playablePathCache[path] = file.path; + return file.path; + }(); + _playablePathTasks[path] = future; + try { + return await future; + } finally { + _playablePathTasks.remove(path); + } + } + + void _scheduleNextTask({Duration delay = Duration.zero}) { + if (_dis) { + return; + } + if (delay == Duration.zero) { + Future.microtask(_pn); + return; + } + Future.delayed(delay, _pn); + } + + void _finishCurrentTask({Duration delay = Duration.zero}) { + if (_dis) { + return; + } + _play = false; + _currentTask = null; + _scheduleNextTask(delay: delay); + if (delay == Duration.zero) { + Future.microtask(_drainPreloadQueue); + } else { + Future.delayed(delay, _drainPreloadQueue); + } + } + + // 绑定控制器 + void bindVapCtrl(VapController vapController) { + _mute = DataPersistence.getPlayGiftMusic(); + _dis = false; + _rgc = vapController; + _log( + 'bindVapCtrl hasVapCtrl=${_rgc != null} ' + 'hasSvgaCtrl=${_rsc != null} queue=${_tq.length} mute=$_mute', + ); + _rgc?.setAnimListener( + onVideoStart: () { + _log('vap onVideoStart path=${_currentTask?.path}'); + }, + onVideoComplete: () { + _log('vap onVideoComplete path=${_currentTask?.path}'); + _hcs(); + }, + onFailed: (code, type, msg) { + _log( + 'vap onFailed path=${_currentTask?.path} code=$code type=$type msg=$msg', + ); + _hcs(); + }, + ); + _scheduleNextTask(); + _drainPreloadQueue(); + } + + void bindSvgaCtrl(SVGAAnimationController svgaController) { + _dis = false; + _rsc = svgaController; + _log( + 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' + 'hasVapCtrl=${_rgc != null} queue=${_tq.length}', + ); + _rsc?.addStatusListener((AnimationStatus status) { + if (status.isCompleted) { + _log('svga completed path=${_currentTask?.path}'); + _rsc?.reset(); + _play = false; + _currentTask = null; + _pn(); + } + }); + _scheduleNextTask(); + _drainPreloadQueue(); + } + + // 播放任务 + void play( + String path, { + int priority = 0, + Map? customResources, + int type = 0, + }) { + if (path.isEmpty) { + _log('play ignored because path is empty'); + return; + } + _log( + 'play request path=$path ext=${SCPathUtils.getFileExtension(path)} ' + 'priority=$priority type=$type ' + 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' + 'disposed=$_dis playing=$_play queueBefore=${_tq.length} ' + 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}', + ); + if (SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim) { + if (_dis) { + _log('play ignored because manager is disposed path=$path'); + return; + } + final task = SCVapTask( + path: path, + priority: priority, + customResources: customResources, + ); + + _tq.add(task); + _log('task enqueued path=$path queueAfter=${_tq.length}'); + if (!_play) { + _pn(); + } + } else { + _log( + 'play ignored because sdkInt <= maxSdkNoAnim ' + 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim}', + ); + } + } + + // 播放下一个任务 + Future _pn() async { + if (_pause) { + _log('skip _pn because paused queue=${_tq.length}'); + return; + } + if (_dis || _tq.isEmpty || _play) return; + + final task = _tq.first; + if (task == null || !_isControllerReady(task)) { + _log( + 'controller not ready for path=${task?.path} ' + 'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} ' + 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} ' + 'queue=${_tq.length}', + ); + return; + } + + _tq.removeFirst(); + _play = true; + _currentTask = task; + try { + final pathType = SCPathUtils.getPathType(task.path); + _log( + 'start task path=${task.path} ' + 'pathType=$pathType ' + 'queueRemaining=${_tq.length} ' + 'needSvga=${_needsSvgaController(task.path)}', + ); + if (pathType == PathType.asset) { + await _pa(task); + } else if (pathType == PathType.file) { + await _pf(task); + } else if (pathType == PathType.network) { + await _pnw(task); + } else { + debugPrint('VAP_SVGA不支持的路径类型: ${task.path}'); + _finishCurrentTask(); + } + } catch (e, s) { + debugPrint('VAP_SVGA播放失败: $e\n$s'); + _finishCurrentTask(); + } + } + + // 播放资源文件 + Future _pa(SCVapTask task) async { + if (_dis) return; + if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { + _log('play asset svga path=${task.path}'); + final entity = await _loadSvgaEntity(task.path); + _log('use prepared asset svga path=${task.path}'); + _rsc?.videoItem = entity; + _rsc?.reset(); + _rsc?.forward(); + _log('forward asset svga path=${task.path}'); + } else { + _log('play asset vap/mp4 path=${task.path}'); + if (task.customResources != null) { + task.customResources?.forEach((k, v) async { + await _rgc?.setVapTagContent(k, v); + }); + } + await _rgc?.playAsset(task.path); + } + } + + // 播放本地文件 + Future _pf(SCVapTask task) async { + if (_dis) { + return; + } + if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { + final entity = await _loadSvgaEntity(task.path); + _log('play local svga file path=${task.path}'); + _rsc?.videoItem = entity; + _rsc?.reset(); + _rsc?.forward(); + return; + } + if (_rgc == null) { + _log('skip playFile because vap controller is null path=${task.path}'); + _finishCurrentTask(); + return; + } + _log('play local vap/mp4 file path=${task.path}'); + await _rgc?.setMute(_mute); + if (task.customResources != null) { + task.customResources?.forEach((k, v) async { + await _rgc?.setVapTagContent(k, v); + }); + } + await _rgc!.playFile(task.path); + } + + // 播放网络资源 + Future _pnw(SCVapTask task) async { + if (_dis) return; + if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { + _log('play network svga path=${task.path}'); + late final MovieEntity entity; + try { + entity = await _loadSvgaEntity(task.path); + } catch (e) { + debugPrint('svga解析出错:$e'); + _finishCurrentTask(); + return; + } + _log('use prepared network svga path=${task.path}'); + _rsc?.videoItem = entity; + _rsc?.reset(); + _rsc?.forward(); + _log('forward network svga path=${task.path}'); + } else { + _log('download network vap/mp4 path=${task.path}'); + final playablePath = await _ensurePlayableFilePath(task.path); + if (!_dis) { + _log('use prepared network vap/mp4 local path=$playablePath'); + await _pf( + SCVapTask( + path: playablePath, + priority: task.priority, + customResources: task.customResources, + ), + ); + } + } + } + + // 处理控制器状态变化 + void _hcs() { + if (_rgc != null && !_dis) { + _log('finish vap task path=${_currentTask?.path}'); + _finishCurrentTask(delay: const Duration(milliseconds: 50)); + } + } + + //暂停动画播放 + void pauseAnim() { + _pause = true; + _log('pauseAnim queue=${_tq.length}'); + } + + //恢复动画播放 + void resumeAnim() { + _pause = false; + _log('resumeAnim queue=${_tq.length}'); + _pn(); + } + + // 释放资源 + void dispose() { + _log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}'); + _dis = true; + _play = false; + _currentTask = null; + _tq.clear(); + _preloadQueue.clear(); + _queuedPreloadPaths.clear(); + _activePreloadCount = 0; + _rgc?.stop(); + _rgc?.dispose(); + _rgc = null; + _rsc?.stop(); + _rsc?.dispose(); + _rsc = null; + } + + // 清除所有任务 + void clearTasks() { + _log( + 'clearTasks queueBefore=${_tq.length} currentPath=${_currentTask?.path}', + ); + _play = false; + _currentTask = null; + _tq.clear(); + _preloadQueue.clear(); + _queuedPreloadPaths.clear(); + _activePreloadCount = 0; + _pause = false; + } +} + +// 任务模型 +// 1. 修改 SCVapTask 类,实现 Comparable + +class SCVapTask implements Comparable { + final String path; + final int type; + final int priority; + final Map? customResources; + final int _seq; + + static int _nextSeq = 0; + + SCVapTask({ + required this.path, + this.priority = 0, + this.customResources, + this.type = 0, + }) : _seq = _nextSeq++; + + @override + int compareTo(SCVapTask other) { + // 先按优先级降序排列 + int priorityComparison = other.priority.compareTo(priority); + if (priorityComparison != 0) { + return priorityComparison; + } + // 相同优先级时,按序列号升序排列(先添加的先执行) + return _seq.compareTo(other._seq); + } + + @override + String toString() { + return 'SCVapTask{path: $path, priority: $priority, seq: $_seq}'; + } +} + +// 优先队列实现 +class SCPriorityQueue { + final List _els = []; + final Comparator _cmp; + + SCPriorityQueue(this._cmp); + + void add(E element) { + _els.add(element); + _els.sort(_cmp); + } + + E removeFirst() { + if (isEmpty) throw StateError("No elements"); + return _els.removeAt(0); + } + + E? get first => isEmpty ? null : _els.first; + + bool get isEmpty => _els.isEmpty; + + bool get isNotEmpty => _els.isNotEmpty; + + int get length => _els.length; + + void clear() => _els.clear(); + + List get unorderedElements => List.from(_els); + + // 实现 Iterable 接口 + Iterator get iterator => _els.iterator; +} diff --git a/lib/shared/tools/sc_path_utils.dart b/lib/shared/tools/sc_path_utils.dart index 7db0619..f2f4736 100644 --- a/lib/shared/tools/sc_path_utils.dart +++ b/lib/shared/tools/sc_path_utils.dart @@ -52,6 +52,7 @@ class SCPathUtils { '.gif', '.mp4', '.vap', + '.svga', ].contains(ext); return isDirectAsset && isSupportedAsset; @@ -74,6 +75,7 @@ class SCPathUtils { '.mp4', '.mov', '.avi', + '.svga', '.png', '.jpg', '.txt', @@ -132,7 +134,6 @@ class SCPathUtils { return false; } - static bool fileTypeIsPic(String filePath) { String extension = path.extension(filePath).toLowerCase(); diff --git a/lib/shared/tools/sc_room_profile_cache.dart b/lib/shared/tools/sc_room_profile_cache.dart new file mode 100644 index 0000000..f4f6fed --- /dev/null +++ b/lib/shared/tools/sc_room_profile_cache.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; + +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' + as follow; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' + as join; +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; + +class SCRoomProfileCache { + static const String _prefix = "room_profile_override"; + + static String _key(String roomId) { + final userId = + AccountStorage().getCurrentUser()?.userProfile?.id ?? "guest"; + return "${_prefix}_${userId}_$roomId"; + } + + static String? preferNonEmpty(String? primary, String? fallback) { + if ((primary ?? "").trim().isNotEmpty) { + return primary; + } + if ((fallback ?? "").trim().isNotEmpty) { + return fallback; + } + return primary ?? fallback; + } + + static String? preferCachedValue(String? cached, String? remote) { + if ((cached ?? "").trim().isNotEmpty) { + return cached; + } + return preferNonEmpty(remote, cached); + } + + static Map getRoomProfile(String roomId) { + if (roomId.trim().isEmpty) { + return {}; + } + final raw = DataPersistence.getString(_key(roomId)); + if (raw.isEmpty) { + return {}; + } + try { + final data = jsonDecode(raw); + if (data is Map) { + return data.map( + (key, value) => MapEntry(key.toString(), value?.toString() ?? ""), + ); + } + } catch (_) {} + return {}; + } + + static Future saveRoomProfile({ + required String roomId, + String? roomCover, + String? roomName, + String? roomDesc, + }) async { + if (roomId.trim().isEmpty) { + return; + } + final current = getRoomProfile(roomId); + final payload = { + "roomCover": preferNonEmpty(roomCover, current["roomCover"]) ?? "", + "roomName": preferNonEmpty(roomName, current["roomName"]) ?? "", + "roomDesc": preferNonEmpty(roomDesc, current["roomDesc"]) ?? "", + }; + await DataPersistence.setString(_key(roomId), jsonEncode(payload)); + } + + static SocialChatRoomRes applyToSocialChatRoomRes(SocialChatRoomRes room) { + final cache = getRoomProfile(room.id ?? ""); + if (cache.isEmpty) { + return room; + } + return room.copyWith( + roomCover: preferCachedValue(cache["roomCover"], room.roomCover), + roomName: preferCachedValue(cache["roomName"], room.roomName), + roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc), + ); + } + + static follow.FollowRoomRes applyToFollowRoomRes(follow.FollowRoomRes room) { + final roomProfile = room.roomProfile; + if (roomProfile == null) { + return room; + } + final cache = getRoomProfile(roomProfile.id ?? ""); + if (cache.isEmpty) { + return room; + } + return room.copyWith( + roomProfile: roomProfile.copyWith( + roomCover: preferCachedValue(cache["roomCover"], roomProfile.roomCover), + roomName: preferCachedValue(cache["roomName"], roomProfile.roomName), + roomDesc: preferCachedValue(cache["roomDesc"], roomProfile.roomDesc), + ), + ); + } + + static MyRoomRes applyToMyRoomRes(MyRoomRes room) { + final cache = getRoomProfile(room.id ?? ""); + if (cache.isEmpty) { + return room; + } + return room.copyWith( + roomCover: preferCachedValue(cache["roomCover"], room.roomCover), + roomName: preferCachedValue(cache["roomName"], room.roomName), + roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc), + ); + } + + static join.JoinRoomRes applyToJoinRoomRes(join.JoinRoomRes room) { + final roomProfile = room.roomProfile; + final basicRoomProfile = roomProfile?.roomProfile; + if (roomProfile == null || basicRoomProfile == null) { + return room; + } + final cache = getRoomProfile(basicRoomProfile.id ?? ""); + if (cache.isEmpty) { + return room; + } + return room.copyWith( + roomProfile: roomProfile.copyWith( + roomProfile: basicRoomProfile.copyWith( + roomCover: preferCachedValue( + cache["roomCover"], + basicRoomProfile.roomCover, + ), + roomName: preferCachedValue( + cache["roomName"], + basicRoomProfile.roomName, + ), + roomDesc: preferCachedValue( + cache["roomDesc"], + basicRoomProfile.roomDesc, + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart index a8a38dd..61c9031 100644 --- a/lib/ui_kit/components/sc_compontent.dart +++ b/lib/ui_kit/components/sc_compontent.dart @@ -11,9 +11,18 @@ import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +const String kRoomCoverDefaultImg = "sc_images/general/sc_no_data.png"; + +String resolveRoomCoverUrl(String? roomId, String? roomCover) { + final cache = SCRoomProfileCache.getRoomProfile(roomId ?? ""); + return SCRoomProfileCache.preferCachedValue(cache["roomCover"], roomCover) ?? + ""; +} + Widget head({ required String url, required double width, @@ -26,52 +35,99 @@ Widget head({ bool showDefault = true, bool isRoom = false, }) { + final bool hasPictureHeaddress = + headdress != null && + headdress.isNotEmpty && + SCPathUtils.fileTypeIsPic2(headdress); + final double avatarScale = + hasPictureHeaddress + ? 0.64 + : (headdress != null && headdress.isNotEmpty ? 0.72 : 1); + final double avatarWidth = width * avatarScale; + final double avatarHeight = (height ?? width) * avatarScale; + + Widget avatar = SizedBox( + width: avatarWidth, + height: avatarHeight, + child: netImage( + url: url, + width: avatarWidth, + height: avatarHeight, + fit: fit, + noDefaultImg: !showDefault, + defaultImg: "sc_images/general/sc_icon_avar_defalt.png", + ), + ); + + if (shape == BoxShape.circle) { + avatar = ClipOval(child: avatar); + if (border != null) { + avatar = Container( + width: avatarWidth, + height: avatarHeight, + decoration: ShapeDecoration(shape: CircleBorder(side: border.top)), + clipBehavior: Clip.antiAlias, + child: avatar, + ); + } + } else if (borderRadius != null) { + avatar = ClipRRect(borderRadius: borderRadius, child: avatar); + if (border != null) { + avatar = Container( + width: avatarWidth, + height: avatarHeight, + decoration: BoxDecoration(border: border, borderRadius: borderRadius), + clipBehavior: Clip.antiAlias, + child: avatar, + ); + } + } else if (border != null) { + avatar = Container( + width: avatarWidth, + height: avatarHeight, + decoration: BoxDecoration(border: border), + clipBehavior: Clip.antiAlias, + child: avatar, + ); + } + return RepaintBoundary( child: Stack( alignment: Alignment.center, children: [ - Container( - padding: EdgeInsets.all(width * 0.12), - width: width, - height: height ?? width, - child: ExtendedImage.network( - url, - width: width, - height: height ?? width, - fit: fit, - cache: true, - shape: shape, - border: border, - clearMemoryCacheWhenDispose: false, - clearMemoryCacheIfFailed: true, - borderRadius: borderRadius, - loadStateChanged: (ExtendedImageState state) { - if (state.extendedImageLoadState == LoadState.completed) { - return ExtendedRawImage( - image: state.extendedImageInfo?.image, - fit: fit, - ); - } else if (state.extendedImageLoadState == LoadState.loading) { - if (showDefault) { - return ExtendedImage.asset( - shape: shape, - "sc_images/general/sc_icon_loading.png", - fit: BoxFit.cover, - ); - } - return Container(); - } else { - return ExtendedImage.asset( - shape: shape, - "sc_images/general/sc_icon_avar_defalt.png", - fit: BoxFit.cover, - ); - } - }, - ), - ), - headdress != null && SCPathUtils.fileTypeIsPic2(headdress) - ? netImage(url: headdress, width: width, height: width) + avatar, + hasPictureHeaddress + ? ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + -1, + -1, + -1, + 3, + 0, + ]), + child: netImage( + url: headdress, + width: width, + height: height ?? width, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ) : Container(), headdress != null && SCPathUtils.getFileExtension(headdress).toLowerCase() == @@ -86,7 +142,8 @@ Widget head({ ) : Container(), headdress != null && - SCPathUtils.getFileExtension(headdress).toLowerCase() == ".mp4" && + SCPathUtils.getFileExtension(headdress).toLowerCase() == + ".mp4" && SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim ? SizedBox( width: width, @@ -301,7 +358,7 @@ searchWidget({ "sc_images/index/sc_icon_serach2.png", width: 20.w, color: serachIconColor ?? Color(0xff18F2B1), - height: 20.w + height: 20.w, ), SizedBox(width: width(6)), _buildPhoneInput( @@ -343,7 +400,10 @@ _buildPhoneInput( cursorColor: SocialChatTheme.primaryColor, decoration: InputDecoration( hintText: tint, - hintStyle: TextStyle(color: SocialChatTheme.textSecondary, fontSize: hintSize ?? sp(14)), + hintStyle: TextStyle( + color: SocialChatTheme.textSecondary, + fontSize: hintSize ?? sp(14), + ), counterText: '', isDense: true, filled: false, diff --git a/lib/ui_kit/components/sc_float_ichart.dart b/lib/ui_kit/components/sc_float_ichart.dart index 735166b..fccb3df 100644 --- a/lib/ui_kit/components/sc_float_ichart.dart +++ b/lib/ui_kit/components/sc_float_ichart.dart @@ -12,211 +12,215 @@ import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; - -// 默认位置 -Offset kDefaultFloatOffset = Offset( - ScreenUtil().screenWidth - width(90), - ScreenUtil().screenHeight - height(120), -); - -class SCFloatIchart { - /// 单例模式 - static final SCFloatIchart _SCFloatIchart = SCFloatIchart._internal(); //1 - factory SCFloatIchart() { - return _SCFloatIchart; - } - - SCFloatIchart._internal(); - - bool _inserted = false; - OverlayEntry? overlayEntry; - BuildContext? context; - Offset offset = kDefaultFloatOffset; - - show() { - if (overlayEntry != null) { - remove(); // 先移除已有的 - } - var overlayState = Overlay.of(context!); - overlayEntry = OverlayEntry( + +// 默认位置 +Offset kDefaultFloatOffset = Offset( + ScreenUtil().screenWidth - width(90), + ScreenUtil().screenHeight - height(120), +); + +class SCFloatIchart { + /// 单例模式 + static final SCFloatIchart _SCFloatIchart = SCFloatIchart._internal(); //1 + factory SCFloatIchart() { + return _SCFloatIchart; + } + + SCFloatIchart._internal(); + + bool _inserted = false; + OverlayEntry? overlayEntry; + BuildContext? context; + Offset offset = kDefaultFloatOffset; + + show() { + if (overlayEntry != null) { + remove(); // 先移除已有的 + } + var overlayState = Overlay.of(context!); + overlayEntry = OverlayEntry( builder: (context) { if (SCNavigatorUtils.inLoginPage) { remove(); return Container(); } else { - return buildToastLayout(); - } - }, - ); - overlayState.insert(overlayEntry!); - _inserted = true; // 标记已插入 - } - - bool isShow() { - return _inserted; - } - - remove() { - if (overlayEntry != null && overlayEntry!.mounted) { - if (_inserted) { - overlayEntry?.remove(); - _inserted = false; - } - } - } - - LayoutBuilder buildToastLayout() { - Timer? timer; - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - Positioned( - left: offset.dx, - top: offset.dy, - child: GestureDetector( - //更新child的位置 - onPanUpdate: (details) { - var localPosition = details.delta; - var dx = (offset.dx + localPosition.dx).clamp( - 0.0, - ScreenUtil().screenWidth - width(90), - ); - var dy = (offset.dy + localPosition.dy).clamp( - 0.0, - ScreenUtil().screenHeight - height(55) - kToolbarHeight, - ); - offset = Offset(dx, dy); - overlayEntry!.markNeedsBuild(); - }, - //拖动结束,处理child贴边悬浮 - onPanEnd: (details) { - var oldPosition = offset.dx; - var targets = - offset.dx + width(80) > (ScreenUtil().screenWidth / 2) - ? ScreenUtil().screenWidth - width(90) - : 0; - timer = Timer.periodic(Duration(milliseconds: 1), (t) { - if (targets > 0) { - oldPosition++; - } else { - oldPosition--; - } - offset = Offset(oldPosition.toDouble(), offset.dy); - overlayEntry!.markNeedsBuild(); - if (oldPosition < 0 || - oldPosition > ScreenUtil().screenWidth - width(90)) { - timer?.cancel(); - } - }); - }, - child: Container( - child: float(), - margin: EdgeInsets.only(bottom: height(kToolbarHeight)), - ), - ), - ), - ], - ); - }, - ); - } - - Widget float() { - JoinRoomRes? room = - Provider.of( - context!, - listen: false, - ).currenRoom; - // User user = Provider.of(context, listen: false).maiMap[0]; - SocialChatLoginRes? user = AccountStorage().getCurrentUser(); - return FloatRoomWindow(room: room, user: user, remove: remove); - } - - void init(BuildContext context) { - this.context = context; - } -} - -class FloatRoomWindow extends StatefulWidget { - final JoinRoomRes? room; - final SocialChatLoginRes? user; - final Function remove; - - const FloatRoomWindow({Key? key, this.room, this.user, required this.remove}) - : super(key: key); - - @override - _FloatRoomWindowState createState() => _FloatRoomWindowState(); -} - -class _FloatRoomWindowState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SCDebounceWidget( - onTap: () { - SCRoomUtils.openCurrentRoom(context); - }, - child: Container( - height: 55.w, - width: 90.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.w), - color: SocialChatTheme.primaryLight, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(width: 8.w), - Stack( - alignment: Alignment.center, - children: [ - netImage( - url: widget.room?.roomProfile?.roomProfile?.roomCover ?? "", - width: 40.w, - height: 40.w, - borderRadius: BorderRadius.circular(8.w), - ), - Image.asset( - "sc_images/general/sc_icon_online_user.png", - width: 15.w, - height: 15.w, - ), - ], - ), - SizedBox(width: width(5)), - SCDebounceWidget( - onTap: () { - Provider.of( - context, - listen: false, - ).exitCurrentVoiceRoomSession(false); - Timer(Duration(milliseconds: 550), () { - widget.remove.call(); - }); - }, - child: Padding( - padding: EdgeInsets.all(5.w), - child: Image.asset( - "sc_images/index/sc_icon_room_flot_close.png", - width: 20.w, - height: 20.w, - ), - ), - ), - ], - ), - ), - ); - } -} + return buildToastLayout(); + } + }, + ); + overlayState.insert(overlayEntry!); + _inserted = true; // 标记已插入 + } + + bool isShow() { + return _inserted; + } + + remove() { + if (overlayEntry != null && overlayEntry!.mounted) { + if (_inserted) { + overlayEntry?.remove(); + _inserted = false; + } + } + } + + LayoutBuilder buildToastLayout() { + Timer? timer; + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + //更新child的位置 + onPanUpdate: (details) { + var localPosition = details.delta; + var dx = (offset.dx + localPosition.dx).clamp( + 0.0, + ScreenUtil().screenWidth - width(90), + ); + var dy = (offset.dy + localPosition.dy).clamp( + 0.0, + ScreenUtil().screenHeight - height(55) - kToolbarHeight, + ); + offset = Offset(dx, dy); + overlayEntry!.markNeedsBuild(); + }, + //拖动结束,处理child贴边悬浮 + onPanEnd: (details) { + var oldPosition = offset.dx; + var targets = + offset.dx + width(80) > (ScreenUtil().screenWidth / 2) + ? ScreenUtil().screenWidth - width(90) + : 0; + timer = Timer.periodic(Duration(milliseconds: 1), (t) { + if (targets > 0) { + oldPosition++; + } else { + oldPosition--; + } + offset = Offset(oldPosition.toDouble(), offset.dy); + overlayEntry!.markNeedsBuild(); + if (oldPosition < 0 || + oldPosition > ScreenUtil().screenWidth - width(90)) { + timer?.cancel(); + } + }); + }, + child: Container( + child: float(), + margin: EdgeInsets.only(bottom: height(kToolbarHeight)), + ), + ), + ), + ], + ); + }, + ); + } + + Widget float() { + JoinRoomRes? room = + Provider.of( + context!, + listen: false, + ).currenRoom; + // User user = Provider.of(context, listen: false).maiMap[0]; + SocialChatLoginRes? user = AccountStorage().getCurrentUser(); + return FloatRoomWindow(room: room, user: user, remove: remove); + } + + void init(BuildContext context) { + this.context = context; + } +} + +class FloatRoomWindow extends StatefulWidget { + final JoinRoomRes? room; + final SocialChatLoginRes? user; + final Function remove; + + const FloatRoomWindow({Key? key, this.room, this.user, required this.remove}) + : super(key: key); + + @override + _FloatRoomWindowState createState() => _FloatRoomWindowState(); +} + +class _FloatRoomWindowState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SCDebounceWidget( + onTap: () { + SCRoomUtils.openCurrentRoom(context); + }, + child: Container( + height: 55.w, + width: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + color: SocialChatTheme.primaryLight, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 8.w), + Stack( + alignment: Alignment.center, + children: [ + netImage( + url: resolveRoomCoverUrl( + widget.room?.roomProfile?.roomProfile?.id, + widget.room?.roomProfile?.roomProfile?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, + width: 40.w, + height: 40.w, + borderRadius: BorderRadius.circular(8.w), + ), + Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 15.w, + height: 15.w, + ), + ], + ), + SizedBox(width: width(5)), + SCDebounceWidget( + onTap: () { + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(false); + Timer(Duration(milliseconds: 550), () { + widget.remove.call(); + }); + }, + child: Padding( + padding: EdgeInsets.all(5.w), + child: Image.asset( + "sc_images/index/sc_icon_room_flot_close.png", + width: 20.w, + height: 20.w, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart index b40a733..9e5758d 100644 --- a/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart +++ b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart @@ -17,10 +17,15 @@ class _VapPlusSvgaPlayerState extends State with TickerProviderStateMixin { late SVGAAnimationController _svgaController; + void _giftFxLog(String message) { + debugPrint('[GiftFX][Widget] $message'); + } + @override void initState() { super.initState(); _svgaController = SVGAAnimationController(vsync: this); + _giftFxLog('initState tag=${widget.tag}'); if (widget.tag == "room_gift") { SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController); } @@ -28,31 +33,43 @@ class _VapPlusSvgaPlayerState extends State @override void dispose() { + _giftFxLog('dispose tag=${widget.tag}'); SCGiftVapSvgaManager().dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - child: IgnorePointer( - // VapView可以通过外层包Container(),设置宽高来限制弹出视频的宽高 - // VapView can set the width and height through the outer package Container() to limit the width and height of the pop-up video - child: VapView( - scaleType: ScaleType.centerCrop, - onViewCreated: (controller) { - if (widget.tag == "room_gift") { - SCGiftVapSvgaManager().bindVapCtrl(controller); - // VapManager().play("https://res.cloudinary.com/dkmchpua1/video/upload/v1737624783/vcg9co6yyfqsadgety1n.mp4"); - } - }, + return IgnorePointer( + child: RepaintBoundary( + child: Stack( + children: [ + Positioned.fill( + child: RepaintBoundary( + child: VapView( + scaleType: ScaleType.fitCenter, + onViewCreated: (controller) { + _giftFxLog('onViewCreated tag=${widget.tag}'); + if (widget.tag == "room_gift") { + SCGiftVapSvgaManager().bindVapCtrl(controller); + // VapManager().play("https://res.cloudinary.com/dkmchpua1/video/upload/v1737624783/vcg9co6yyfqsadgety1n.mp4"); + } + }, + ), + ), ), - ), + Positioned.fill( + child: RepaintBoundary( + child: SVGAImage( + _svgaController, + fit: BoxFit.contain, + allowDrawingOverflow: false, + ), + ), + ), + ], ), - Positioned.fill(child: SVGAImage(_svgaController, fit: BoxFit.fill)), - ], + ), ); } } diff --git a/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart index 5caf91a..da32038 100644 --- a/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart @@ -125,7 +125,7 @@ class _FloatingGiftScreenWidgetState extends State SCRoomUtils.goRoom( widget.message.roomId!, navigatorKey.currentState!.context, - fromFloting: true + fromFloting: true, ); } }, @@ -157,7 +157,7 @@ class _FloatingGiftScreenWidgetState extends State padding: EdgeInsets.symmetric(horizontal: 10.w), decoration: BoxDecoration( image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_gift_flosc_bg.png"), + image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), fit: BoxFit.fill, ), ), @@ -278,18 +278,18 @@ class _FloatingGiftScreenWidgetState extends State String _getLuckGiftFloatBg() { if ((widget.message.coins ?? 0) > 4999 && (widget.message.coins ?? 0) < 10000) { - return "sc_images/room/sc_icon_luck_gift_flosc_bg1.png"; + return "sc_images/room/sc_icon_luck_gift_float_bg1.png"; } else if ((widget.message.coins ?? 0) > 9999 && (widget.message.coins ?? 0) < 75000) { - return "sc_images/room/sc_icon_luck_gift_flosc_bg2.png"; + return "sc_images/room/sc_icon_luck_gift_float_bg2.png"; } else if ((widget.message.coins ?? 0) > 74999 && (widget.message.coins ?? 0) < 150000) { - return "sc_images/room/sc_icon_luck_gift_flosc_bg3.png"; + return "sc_images/room/sc_icon_luck_gift_float_bg3.png"; } else if ((widget.message.coins ?? 0) > 149999 && (widget.message.coins ?? 0) < 1500000) { - return "sc_images/room/sc_icon_luck_gift_flosc_bg4.png"; + return "sc_images/room/sc_icon_luck_gift_float_bg4.png"; } else if ((widget.message.coins ?? 0) > 1499999) { - return "sc_images/room/sc_icon_luck_gift_flosc_bg5.png"; + return "sc_images/room/sc_icon_luck_gift_float_bg5.png"; } return ""; diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index e01a2bb..e9582d5 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -127,7 +127,7 @@ class _FloatingLuckGiftScreenWidgetState SCRoomUtils.goRoom( widget.message.roomId!, navigatorKey.currentState!.context, - fromFloting: true + fromFloting: true, ); } }, @@ -161,7 +161,7 @@ class _FloatingLuckGiftScreenWidgetState flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 flipY: false, // 垂直翻转设为 false child: Image.asset( - "sc_images/room/sc_icon_luck_gift_flosc_n_bg.png", + "sc_images/room/sc_icon_luck_gift_float_n_bg.png", fit: BoxFit.fill, ), ), @@ -322,7 +322,7 @@ class _FloatingLuckGiftScreenWidgetState SCRoomUtils.goRoom( widget.message.roomId!, navigatorKey.currentState!.context, - fromFloting: true + fromFloting: true, ); } }, @@ -354,7 +354,7 @@ class _FloatingLuckGiftScreenWidgetState padding: EdgeInsets.symmetric(horizontal: 15.w), decoration: BoxDecoration( image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_gift_flosc_bg.png"), + image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), fit: BoxFit.fill, ), ), diff --git a/lib/ui_kit/widgets/room/room_head_widget.dart b/lib/ui_kit/widgets/room/room_head_widget.dart index cf9c747..cb7d35d 100644 --- a/lib/ui_kit/widgets/room/room_head_widget.dart +++ b/lib/ui_kit/widgets/room/room_head_widget.dart @@ -47,13 +47,19 @@ class _RoomHeadWidgetState extends State { alignment: Alignment.center, children: [ netImage( - url: - provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomCover ?? - "", + url: resolveRoomCoverUrl( + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id, + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomCover, + ), + defaultImg: kRoomCoverDefaultImg, width: 28.w, height: 28.w, borderRadius: BorderRadius.all( diff --git a/lib/ui_kit/widgets/room/room_online_user_widget.dart b/lib/ui_kit/widgets/room/room_online_user_widget.dart index d7e05af..e81f9a2 100644 --- a/lib/ui_kit/widgets/room/room_online_user_widget.dart +++ b/lib/ui_kit/widgets/room/room_online_user_widget.dart @@ -1,173 +1,176 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:provider/provider.dart'; - -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; -import 'package:yumi/services/room/rc_room_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/modules/room/online/room_online_page.dart'; -import 'package:yumi/modules/room/rank/room_gift_rank_page.dart'; - -class RoomOnlineUserWidget extends StatefulWidget { - @override - _RoomOnlineUserWidgetState createState() => _RoomOnlineUserWidgetState(); -} - -class _RoomOnlineUserWidgetState extends State { - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, child) { - return Row( - children: [ - _buildExperience(), - SizedBox(width: 15.w), - ref.onlineUsers.isNotEmpty - ? Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - SizedBox( - height: 25.w, - child: Stack( - clipBehavior: Clip.none, - children: [ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - ref.onlineUsers.length, - (index) { - return Transform.translate( - offset: Offset(-3.w * index, 0), - child: Padding( - padding: EdgeInsets.only(right: 0.w), - child: netImage( - url: - ref - .onlineUsers[index] - .userAvatar ?? - "", - width: 23.w, - height: 23.w, - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 1.w, - ), - ), - ), - ); - }, - ), - ), - ), - ], - ), - ), - GestureDetector( - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 3.w, - vertical: 3.w, - ), - child: Column( - children: [ - Image.asset( - "sc_images/room/sc_icon_online_peple.png", - width: 12.w, - height: 12.sp, - ), - text( - "${ref.onlineUsers.length}", - fontSize: 9.sp, - ), - ], - ), - ), - onTap: () { - showBottomInBottomDialog( - context, - RoomOnlinePage( - roomId: - ref - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id, - ), - ); - }, - ), - SizedBox(width: 5.w,) - ], - ), - ) - : Container(), - ], - ); - }, - ); - } - - ///房间榜单入口 - _buildExperience() { - return Consumer( - builder: (context, ref, child) { - return GestureDetector( - child: Container( - width: 90.w, - height: 27.w, - decoration: BoxDecoration( - color: Colors.white10, - borderRadius: BorderRadiusDirectional.only( - topEnd: Radius.circular(20.w), - bottomEnd: Radius.circular(20.w), - ), - ), - alignment: AlignmentDirectional.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/room/sc_icon_room_contribute.png", - width: 18.w, - height: 18.w, - ), - SizedBox(width: 5.w), - text( - "${ref.roomContributeLevelRes?.thisWeekIntegral ?? 0}", - fontSize: 13.sp, - textColor: Colors.orangeAccent, - fontWeight: FontWeight.w600, - ), - Icon( - Icons.chevron_right, - size: 13.w, - color: Colors.orangeAccent, - ), - ], - ), - ), - onTap: () { - SmartDialog.show( - tag: "showRoomGiftRankPage", - alignment: Alignment.bottomCenter, - animationType: SmartAnimationType.fade, - builder: (_) { - return RoomGiftRankPage(); - }, - ); - }, - ); - }, - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/services/room/rc_room_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/modules/room/online/room_online_page.dart'; +import 'package:yumi/modules/room/rank/room_gift_rank_page.dart'; + +class RoomOnlineUserWidget extends StatefulWidget { + const RoomOnlineUserWidget({super.key}); + + @override + State createState() => _RoomOnlineUserWidgetState(); +} + +class _RoomOnlineUserWidgetState extends State { + void _openRoomOnlinePage(RtcProvider ref) { + showBottomInBottomDialog( + context, + RoomOnlinePage(roomId: ref.currenRoom?.roomProfile?.roomProfile?.id), + ); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return Row( + children: [ + _buildExperience(), + SizedBox(width: 15.w), + ref.onlineUsers.isNotEmpty + ? Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _openRoomOnlinePage(ref), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 25.w, + child: Stack( + clipBehavior: Clip.none, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + ref.onlineUsers.length, + (index) { + return Transform.translate( + offset: Offset(-3.w * index, 0), + child: Padding( + padding: EdgeInsets.only( + right: 0.w, + ), + child: netImage( + url: + ref + .onlineUsers[index] + .userAvatar ?? + "", + width: 23.w, + height: 23.w, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 1.w, + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 3.w, + ), + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_online_peple.png", + width: 12.w, + height: 12.sp, + ), + text( + "${ref.onlineUsers.length}", + fontSize: 9.sp, + ), + ], + ), + ), + ], + ), + ), + SizedBox(width: 5.w), + ], + ), + ) + : Container(), + ], + ); + }, + ); + } + + ///房间榜单入口 + _buildExperience() { + return Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Container( + width: 90.w, + height: 27.w, + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadiusDirectional.only( + topEnd: Radius.circular(20.w), + bottomEnd: Radius.circular(20.w), + ), + ), + alignment: AlignmentDirectional.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/room/sc_icon_room_contribute.png", + width: 18.w, + height: 18.w, + ), + SizedBox(width: 5.w), + text( + "${ref.roomContributeLevelRes?.thisWeekIntegral ?? 0}", + fontSize: 13.sp, + textColor: Colors.orangeAccent, + fontWeight: FontWeight.w600, + ), + Icon( + Icons.chevron_right, + size: 13.w, + color: Colors.orangeAccent, + ), + ], + ), + ), + onTap: () { + SmartDialog.show( + tag: "showRoomGiftRankPage", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return RoomGiftRankPage(); + }, + ); + }, + ); + }, + ); + } +} diff --git a/sc_images/general/sc_no_data.png b/sc_images/general/sc_no_data.png new file mode 100644 index 0000000000000000000000000000000000000000..0db4d5f50798b5db8b265da4ecd2bf8f0359f42d GIT binary patch literal 41895 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
gAZ%1+@C*$-_skKdQhnxCY01u0A|?e$Sp-?aG?v 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

op7ZE7%7qnyEdZ_W zZeYiS<+W+Nfx#u*1CU0e)1|f((YO)|AX$`(IDe(%VKR;;z`Ef8lt9-oTkhKL%QBG+O4_sEVf_-Vl zEMq%0$4*MEjamuly4yYY2B5JzkV-4hoMp2?)6nDgaU#SO75t-l15>A<#WrB!V6dhg8LC#jRq^ zK|r{RMiXQ%Rx2n}#g+)jP6;9)uZ;l^D+1*O1k(Ya%|`|7whp9r%K>y=Kqf<%{Ly%q zo$;RM42C$*V1ac-ci%sNVk!Wkis4$eKtN-4ptl->T#vLtgHB)S!nxobR586PNs^YB2t2i0(ubxxKTvO7f}F|&qu0`DuBFVg7&fAWK0L3upGT0cc1fl^v(*efbhux zXn8WDZkJzMuzuu;p!a0>R+NkPVgRjD&T%~R;MuPHlsN3^2+){b1Uh#i<#P^Fg8l{M zItDZdpaqO88%s$*=zUdDQMcP|G6Oyjj_Jhs+uk^*@-OlC*y8F;?9N&9u-ZSm{6h%` zS`dNuivb0j$G3+P!~n<;(S4}g5tdO@U0>%CCDu8Iur5~1&pmwwM&d|*27JH93`BelYU+H8x zbRrmJHha5TMJ0+a5y&{E(W@5jxUYd};Rw*ruYhhI2f`Ei1w4Wra~XZ8Uf2?fNYvid z`_Xy%47ZPTV9uXC3^Ubf`_z;n_HE0v*vRbX)6wbDIg z0$VJa9Ui%rGDwtDZc?`3zx&zYs|jc+wK%jUj*u>0WV%@aZFG#1+{U-N&`+HY-cS#*7WdeEY2#Uavq(JMNg z?3<=b?WUQ_J?>*Cg_8`XQlJTHT2oGED*;L6eG%wyw@AEu7;d0w00000NkvXXu0mjf DJ;~U5 literal 0 HcmV?d00001 diff --git a/需求进度.md b/需求进度.md index 5b50932..4a008fe 100644 --- a/需求进度.md +++ b/需求进度.md @@ -5,7 +5,20 @@ ## 已完成模块 - 创建并持续维护进度跟踪文件。 +- 已继续排查语言房 gift 动画链路:确认送礼后会同时走本地房间消息、滚屏礼物条和大额礼物全局飘屏三条路径,并修复动画管理器在控制器尚未绑定完成时提前消费队列导致后续动画不再播放的问题。 +- 已定位并修复语言房礼物飘屏资源引用错误:代码里误写 `sc_icon_gift_flosc_bg` / `sc_icon_luck_gift_flosc_*`,实际资源文件名为 `float`,导致点击送礼后飘屏背景图加载失败,相关动画无法正常显示。 +- 已继续定位语言房礼物特效不播放的根因:后端礼物配置返回的特效标记为 `ANIMATION`,而前端历史代码只识别拼写错误的 `ANIMSCION`,导致送礼后直接跳过全屏/半屏特效播放;现已改为同时兼容两种标记。 +- 已继续优化语言房礼物特效播放性能:将礼物资源预热改为“后台串行预加载 + 选中礼物高优先级预加载 + 播放阶段复用同一解码/下载任务”,避免送礼时重复下载或重复解码同一个 `.svga/.mp4`;同时为全屏播放器增加 `RepaintBoundary`,并改为按原始比例渲染、关闭越界绘制,减少整页重绘和过度放大带来的卡顿。 - 已修复首页房间封面显示链路:兼容接口 `roomCover/cover` 双字段,并补齐编辑房间成功后的封面内存回写。 +- 已继续修复房间设置保存封面后丢失的问题:房间保存接口改为兼容提交 `id/roomId` 与 `roomCover/cover`,并在保存成功后优先保留刚提交的封面、名称、公告,避免被空响应覆盖。 +- 已新增按房间 ID 的本地房间资料缓存兜底:首页、我的房间、历史/关注、搜索和再次进入房间都会优先回填最近一次本地保存的封面;同时已将房间封面空态图替换为新的 `sc_no_data.png`。 +- 已确认房间封面上传接口与 `PUT /room/profile` 保存接口都返回了正确图片地址,并修正首页/重进房间的取值优先级,避免被列表接口短时间返回的旧封面或无效封面重新覆盖。 +- 已继续完善个人主页头像与滚动表现:顶部小头像保持纯头像,主头像恢复头像框叠加;同时将个人主页背景并入可滚动 header,修复滑动时内容与背景不同步的问题。 +- 已进一步统一头像框方案:共享头像组件改为“头像居中 + 头像框包边”结构,并对这类静态头像框资源增加白底去除处理,不再简单把头像框整张盖在头像上;个人主页主头像与房间麦位头像现已同步采用这一展示方式。 +- 已修复静态头像框透明区域误变黑的问题:调整头像框白底透明化公式后,进入房间时不再出现只有头像附近有颜色、其余房间背景被黑块覆盖的现象。 +- 已修复个人主页首屏黑屏与内容区多余圆角问题:为个人页补充稳定的底图/底色占位,避免进入页面时先出现黑屏;同时移除资料内容区顶部多余圆角。 +- 已将个人主页首屏占位升级为骨架屏:进入个人页时会先按真实版式展示头像、昵称、计数区、Tab 和资料内容的骨架块,不再先露默认背景等待约 1 秒。 +- 已优化语言房顶部成员入口的点击范围:右上角在线成员区域现已将左侧头像堆叠区与右侧成员图标统一为同一点击热区,用户点击头像区也可直接打开成员列表页。 - 完成仓库结构、依赖引用、资源体积和 APK 组成的第一轮排查。 - 移除 4 个当前未在业务层直接使用的插件依赖:`loading_indicator_view_plus`、`social_sharing_plus`、`flutter_foreground_task`、`on_audio_query`,并清理相关平台声明。 - 修补 `image_cropper 5.0.1` Android 兼容问题,切换到本地 path 依赖以恢复构建。 @@ -50,12 +63,32 @@ ## 已改动文件 - `需求进度.md` +- `lib/shared/data_sources/models/enum/sc_gift_type.dart` +- `lib/shared/tools/sc_gift_vap_svga_manager.dart` +- `lib/services/general/sc_app_general_manager.dart` +- `lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart` +- `lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart` - `lib/services/room/rc_room_manager.dart` +- `lib/services/audio/rtc_manager.dart` +- `lib/modules/room/edit/room_edit_page.dart` +- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart` +- `lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart` +- `lib/shared/tools/sc_room_profile_cache.dart` - `lib/shared/business_logic/models/res/room_res.dart` - `lib/shared/business_logic/models/res/follow_room_res.dart` - `lib/shared/business_logic/models/res/my_room_res.dart` - `lib/shared/business_logic/models/res/sc_edit_room_info_res.dart` - `lib/shared/business_logic/models/res/join_room_res.dart` +- `lib/ui_kit/components/sc_compontent.dart` +- `lib/ui_kit/components/sc_float_ichart.dart` +- `lib/ui_kit/widgets/room/room_head_widget.dart` +- `lib/modules/room/detail/room_detail_page.dart` +- `lib/modules/home/popular/party/sc_home_party_page.dart` +- `lib/modules/home/popular/follow/sc_room_follow_page.dart` +- `lib/modules/home/popular/history/sc_room_history_page.dart` +- `lib/modules/home/popular/mine/sc_home_mine_page.dart` +- `lib/modules/search/sc_search_page.dart` +- `sc_images/general/sc_no_data.png` - `.gitignore` - `android/key.properties` - `pubspec.yaml` @@ -81,8 +114,21 @@ - `assets/` ## 已验证结果 +- 已通过代码与资源目录交叉核对确认:房内礼物飘屏当前报错 `Unable to load asset: "sc_images/room/sc_icon_gift_flosc_bg.png"` 的根因是资源名拼写错误,而不是消息未下发;工程内实际存在的资源文件为 `sc_icon_gift_float_bg.png`、`sc_icon_luck_gift_float_bg1~5.png`、`sc_icon_luck_gift_float_n_bg.png`。 +- 已确认当前“点击赠送没有动画”至少包含一类前端资源问题:大额礼物/幸运礼物飘屏 widget 已进入渲染,但因为背景图 asset 路径写错而无法显示;全屏 `SVGA/VAP` 特效是否播放仍取决于具体礼物是否带有 `giftSourceUrl`,且 `special` 包含 `ANIMSCION/ANIMATION/GLOBAL_GIFT` 之一。 +- 已完成一轮针对卡顿的代码级优化:礼物列表现在只会对真正需要全屏特效的礼物做受控预热;用户选中礼物时会立即高优先级预热该礼物;播放器播放时优先复用内存里的 `MovieEntity` 或同一路径的在途任务,不再重复发起网络下载/重复解码;渲染层也已改为独立重绘边界以减少房间整页跟随重绘。 +- 已通过 GiftFX 调试日志确认:示例礼物 `阿拉伯舞蹈` 实际返回 `giftSourceUrl=.svga` 且 `special=ANIMATION`,此前前端因为不识别该标记而输出 `skip local play because special does not include ANIMSCION/GLOBAL_GIFT`;本轮已补齐 `ANIMATION` 兼容判断。 - 当前工作区存在用户已有未提交改动,后续处理均避开覆盖。 - 首页 `party/follow/history/mine` 以及房间详情使用的房间封面模型已兼容 `roomCover` 与 `cover` 两种返回字段,上传封面后不会再因为字段名不一致掉回空占位。 +- 房间设置保存封面时,前端现在会同时兼容提交和读取两套字段,并在 `PUT /room/profile` 响应或紧接着的房间详情刷新返回空封面时保留刚上传的封面,不再被空值冲掉。 +- 已新增本地房间封面兜底缓存:即使服务端短时间仍返回空封面,首页列表、我的房间、搜索结果、房间头部、房间详情和再次进入房间也会继续显示最近一次本地成功保存的封面。 +- 已通过实际请求日志确认:`POST /external/oss/upload`、`PUT /room/profile` 和随后 `GET /room/profile/specific` 都已返回正确封面 URL;本轮问题的根因已收敛为首页/列表链路仍可能使用接口短时间回传的旧值,因此本地成功保存的封面现已提升为更高优先级。 +- 房间封面空态图已切换为新的 `sc_images/general/sc_no_data.png`,其资源尺寸为 `288x288`。 +- 房间资料卡与个人主页使用的用户资料链路已重新对齐:个人页现兼容 `avatar` 别名;顶部小头像不叠加头像框,但主头像会继续叠加头像框资源,且个人页背景已随 header 一起滚动,不再出现内容先滑动、背景停留不动的现象。 +- 头像框显示逻辑已改为“框在外、头像在内”的常见样式;针对当前这类带白底的静态头像框资源,已增加白底透明化处理,因此个人主页主头像与房间麦位头像都不会再留下明显白边。 +- 静态头像框的透明区域现已保持透明,不会再被错误算成黑色;房间麦位头像、个人主页主头像和其它复用 `head()` 的头像组件进入房间后不会再出现大块黑底遮罩。 +- 个人主页现在在数据加载前也会先展示默认背景和底色,不会再闪黑;“About me / Giftwall” 内容区顶部边缘已改为直角,不再额外出现一个圆角缺口。 +- 个人主页当前已接入贴合页面结构的骨架屏,占位期间不会先看到背景图单独显示;资料接口返回后再无缝切到真实内容,首屏观感更平滑。 - 目录体积排查显示:`build/` 约 `4.7G`、`.dart_tool/` 约 `347M`、`ios/` 约 `250M`、`sc_images/` 约 `48M`、`local_packages/` 约 `6.1M`。 - 当前包体过大的主要原因已经确认: - universal APK 带入了 `arm64-v8a`、`armeabi-v7a`、`x86_64` 三套 ABI。