diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index 4f23570..05a0836 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -603,6 +603,8 @@ "custom": "مخصص", "customBackground": "خلفية مخصصة", "example": "مثال", + "endPreview": "إنهاء المعاينة", + "selectAgain": "اختر مرة أخرى", "store": "المتجر", "viewFrame": "عرض الإطار", "headdress": "إطارات", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index 3f0b00e..a3adaaf 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -620,6 +620,8 @@ "custom": "কাস্টম", "customBackground": "কাস্টম ব্যাকগ্রাউন্ড", "example": "উদাহরণ", + "endPreview": "প্রিভিউ শেষ করুন", + "selectAgain": "আবার নির্বাচন করুন", "myItems": "আমার আইটেম", "use": "ব্যবহার করুন", "unUse": "ব্যবহার না করুন", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index fc3620f..19fa137 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -620,6 +620,8 @@ "custom": "Custom", "customBackground": "Custom Background", "example": "Example", + "endPreview": "End Preview", + "selectAgain": "Select again", "myItems": "My items", "use": "Use", "unUse": "Unequip", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index e45f451..20715a7 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -620,7 +620,9 @@ "custom": "Özel", "customBackground": "Özel Arka Plan", "example": "Örnek", - "myItems": "Eşyalarım", + "endPreview": "Önizlemeyi Bitir", + "selectAgain": "Tekrar seç", + "myItems": "Eşyalarım", "use": "Kullan", "unUse": "Kullanmama", "renewal": "Yenileme", diff --git a/lib/app/routes/sc_routes.dart b/lib/app/routes/sc_routes.dart index 113b1f7..4f77722 100644 --- a/lib/app/routes/sc_routes.dart +++ b/lib/app/routes/sc_routes.dart @@ -7,6 +7,7 @@ import 'package:yumi/modules/chat/chat_route.dart'; import 'package:yumi/modules/store/store_route.dart'; import 'package:yumi/modules/index/index_page.dart'; import 'package:yumi/modules/user/settings/settings_route.dart'; +import 'package:yumi/modules/user/vip/vip_route.dart'; import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/modules/auth/login_route.dart'; import 'package:yumi/modules/room/voice_room_route.dart'; @@ -15,17 +16,25 @@ import 'package:yumi/app/routes/sc_404_page.dart'; class SCRoutes { static String home = '/'; - static List _listRouter = []; + static final List _listRouter = []; static void configureRoutes(fluro.FluroRouter router) { /// 指定路由跳转错误返回页 - router.notFoundHandler = fluro.Handler(handlerFunc: (BuildContext? context, Map> params) { - debugPrint('未找到目标页'); - return SCWidgetNotFound(); - }); + router.notFoundHandler = fluro.Handler( + handlerFunc: (BuildContext? context, Map> params) { + debugPrint('未找到目标页'); + return SCWidgetNotFound(); + }, + ); - router.define(home, - handler: fluro.Handler(handlerFunc: (BuildContext? context, Map> params) => SCIndexPage())); + router.define( + home, + handler: fluro.Handler( + handlerFunc: + (BuildContext? context, Map> params) => + SCIndexPage(), + ), + ); _listRouter.clear(); @@ -37,6 +46,7 @@ class SCRoutes { _listRouter.add(SettingsRoute()); _listRouter.add(VoiceRoomRoute()); _listRouter.add(SCMainRoute()); + _listRouter.add(VipRoute()); _listRouter.add(WalletRoute()); _listRouter.add(StoreRoute()); _listRouter.add(SCChatRouter()); diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index ae04de6..143d872 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -1326,6 +1326,10 @@ class SCAppLocalizations { String get example => translate('example'); + String get endPreview => translate('endPreview'); + + String get selectAgain => translate('selectAgain'); + String get touristsCannotSendMessages => translate('touristsCannotSendMessages'); diff --git a/lib/modules/auth/edit/sc_edit_profile_page.dart b/lib/modules/auth/edit/sc_edit_profile_page.dart index 485f18d..4444bc3 100644 --- a/lib/modules/auth/edit/sc_edit_profile_page.dart +++ b/lib/modules/auth/edit/sc_edit_profile_page.dart @@ -604,13 +604,12 @@ class _SCEditProfilePageState extends State { context, listen: false, ).uid; - // TODO(register): When the backend registration API supports invitation - // codes, pass `invitationCodeController.text.trim()` here as an optional - // argument. Empty string should remain allowed. + final invitationCode = invitationCodeController.text.trim(); SocialChatLoginRes user = await SCAccountRepository().regist( authType, idToken, userProvider!.editUser!, + invitationCode: invitationCode, ); final submittedProfile = userProvider?.editUser; if (submittedProfile != null) { diff --git a/lib/modules/room/background/room_background_preview_page.dart b/lib/modules/room/background/room_background_preview_page.dart new file mode 100644 index 0000000..24f7959 --- /dev/null +++ b/lib/modules/room/background/room_background_preview_page.dart @@ -0,0 +1,434 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart'; + +class RoomBackgroundPreviewPage extends StatelessWidget { + const RoomBackgroundPreviewPage({super.key, this.backgroundPath}); + + final String? backgroundPath; + + @override + Widget build(BuildContext context) { + final bottomPadding = MediaQuery.of(context).padding.bottom; + final localizations = SCAppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill(child: _PreviewBackground(path: backgroundPath)), + Positioned.fill(child: _PreviewRoomLayer()), + Positioned( + left: 24.w, + right: 24.w, + bottom: 148.w + bottomPadding, + child: _PreviewPrimaryButton( + text: localizations.endPreview, + onTap: () => SCNavigatorUtils.goBackWithParams(context, true), + ), + ), + Positioned( + left: 24.w, + right: 24.w, + bottom: 90.w + bottomPadding, + child: _PreviewSecondaryButton( + text: localizations.selectAgain, + onTap: () => SCNavigatorUtils.goBackWithParams(context, false), + ), + ), + ], + ), + ); + } +} + +class _PreviewBackground extends StatelessWidget { + const _PreviewBackground({this.path}); + + final String? path; + + @override + Widget build(BuildContext context) { + final value = (path ?? "").trim(); + if (value.startsWith("http://") || value.startsWith("https://")) { + return Image.network( + value, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(), + ); + } + if (value.startsWith("assets/") || value.startsWith("sc_images/")) { + return Image.asset( + value, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(), + ); + } + if (value.isNotEmpty) { + final file = File(value); + if (file.existsSync()) { + return Image.file( + file, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(), + ); + } + } + return const _DefaultPreviewBackground(); + } +} + +class _DefaultPreviewBackground extends StatelessWidget { + const _DefaultPreviewBackground(); + + @override + Widget build(BuildContext context) { + return Image.asset( + SCGlobalConfig.businessLogicStrategy.getVoiceRoomDefaultBackgroundImage(), + fit: BoxFit.cover, + ); + } +} + +class _PreviewRoomLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: ScreenUtil().setWidth(42)), + IgnorePointer(child: RoomHeadWidget()), + SizedBox(height: 5.w), + const IgnorePointer(child: RoomOnlineUserWidget()), + const IgnorePointer(child: _PreviewSeat10()), + Expanded(child: _PreviewChatArea()), + const IgnorePointer(child: RoomBottomWidget()), + ], + ); + } +} + +class _PreviewSeat10 extends StatelessWidget { + const _PreviewSeat10(); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: List.generate( + 5, + (index) => Expanded( + child: _PreviewSeatItem(index: index, locked: index == 1), + ), + ), + ), + Row( + children: List.generate(5, (index) { + final seatIndex = index + 5; + return Expanded( + child: _PreviewSeatItem( + index: seatIndex, + locked: seatIndex == 6, + ), + ); + }), + ), + ], + ), + ); + } +} + +class _PreviewSeatItem extends StatelessWidget { + const _PreviewSeatItem({required this.index, required this.locked}); + + final int index; + final bool locked; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 55.w, + height: 55.w, + child: Image.asset( + locked + ? "sc_images/room/sc_icon_seat_lock.png" + : "sc_images/room/sc_icon_seat_open.png", + width: 52.w, + height: 52.w, + ), + ), + SizedBox( + height: 16.w, + child: Center( + child: Text( + "NO.${index + 1}", + style: TextStyle( + color: Colors.white, + fontSize: 10.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } +} + +class _PreviewChatArea extends StatefulWidget { + @override + State<_PreviewChatArea> createState() => _PreviewChatAreaState(); +} + +class _PreviewChatAreaState extends State<_PreviewChatArea> { + int _selectedTabIndex = 0; + + void _selectTab(int index) { + if (_selectedTabIndex == index) { + return; + } + setState(() { + _selectedTabIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + left: 0, + right: 0, + top: 0, + child: _PreviewChatTabs( + selectedIndex: _selectedTabIndex, + onTap: _selectTab, + ), + ), + Positioned( + left: 24.w, + right: 24.w, + bottom: 166.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _PreviewChatBubble(text: "Hello My Friends!"), + SizedBox(height: 12.w), + _PreviewGiftBubble(), + ], + ), + ), + ], + ); + } +} + +class _PreviewChatTabs extends StatelessWidget { + const _PreviewChatTabs({required this.selectedIndex, required this.onTap}); + + final int selectedIndex; + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32.w, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 8.w), + itemCount: 3, + itemBuilder: (context, index) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onTap(index), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Center( + child: Image.asset( + _assetFor(index, selectedIndex == index), + height: 26.w, + fit: BoxFit.contain, + ), + ), + ), + ); + }, + ), + ); + } + + String _assetFor(int index, bool selected) { + switch (index) { + case 0: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_all_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_all_unselected.png"; + case 1: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_chat_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_chat_unselected.png"; + case 2: + default: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_gift_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_gift_unselected.png"; + } + } +} + +class _PreviewChatBubble extends StatelessWidget { + const _PreviewChatBubble({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxWidth: 276.w), + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.42), + borderRadius: BorderRadius.circular(10.w), + ), + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} + +class _PreviewGiftBubble extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxWidth: 300.w), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.44), + borderRadius: BorderRadius.circular(10.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + "Send To User Name2", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + SizedBox(width: 10.w), + ClipOval( + child: Image.asset( + "sc_images/general/sc_icon_avar_defalt.png", + width: 32.w, + height: 32.w, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 6.w), + Text( + "*1", + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ); + } +} + +class _PreviewPrimaryButton extends StatelessWidget { + const _PreviewPrimaryButton({required this.text, required this.onTap}); + + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999.w), + gradient: LinearGradient( + colors: [SocialChatTheme.primaryLight, const Color(0xff8BF2D0)], + ), + ), + child: Text( + text, + style: TextStyle( + color: const Color(0xff0B2823), + fontSize: 18.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + ); + } +} + +class _PreviewSecondaryButton extends StatelessWidget { + const _PreviewSecondaryButton({required this.text, required this.onTap}); + + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xff06372F).withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(999.w), + border: Border.all(color: SocialChatTheme.primaryLight, width: 1.w), + ), + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + ); + } +} diff --git a/lib/modules/room/background/room_background_select_page.dart b/lib/modules/room/background/room_background_select_page.dart index 97b5ff9..3b0a8d0 100644 --- a/lib/modules/room/background/room_background_select_page.dart +++ b/lib/modules/room/background/room_background_select_page.dart @@ -82,6 +82,17 @@ class _RoomBackgroundSelectPageState extends State }); } + Future _previewItem(List<_RoomBackgroundItem> items, int index) async { + final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview( + context, + backgroundPath: items[index].localImagePath, + ); + if (!mounted || shouldUse != true) { + return; + } + _selectItem(items, index); + } + @override Widget build(BuildContext context) { final localizations = SCAppLocalizations.of(context)!; @@ -114,12 +125,12 @@ class _RoomBackgroundSelectPageState extends State _RoomBackgroundGrid( items: _officialItems, onTap: - (index) => _selectItem(_officialItems, index), + (index) => _previewItem(_officialItems, index), bottomPadding: 24.w, ), _RoomBackgroundGrid( items: _mineItems, - onTap: (index) => _selectItem(_mineItems, index), + onTap: (index) => _previewItem(_mineItems, index), bottomPadding: 100.w, ), ], diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart index 9c1592e..e3f75ae 100644 --- a/lib/modules/room/seat/sc_seat_item.dart +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.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/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_screen.dart'; @@ -475,8 +476,8 @@ class _EmoticonsState extends State with TickerProviderStateMixin { } }, ) - : netImage( - url: playingEvent?.number ?? "", + : _buildEmojiImage( + playingEvent?.number ?? "", width: widget.isGameModel ? 65.w : 75.w, )), ); @@ -495,6 +496,21 @@ class _EmoticonsState extends State with TickerProviderStateMixin { ); } + Widget _buildEmojiImage(String path, {required double width}) { + if (path.isEmpty) { + return SizedBox(width: width, height: width); + } + if (path.startsWith("assets/") || path.startsWith("sc_images/")) { + return RoomEmojiAssetImage( + key: ValueKey(path), + asset: path, + width: width, + height: width, + ); + } + return netImage(url: path, width: width); + } + void _checkStart() { if (!showIn) { if (pathList.isNotEmpty) { diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 5b98fc5..341fcf7 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -67,6 +67,14 @@ class _VoiceRoomPageState extends State _tabController = TabController(length: _pages.length, vsync: this); _enableRoomVisualEffects(); _tabController.addListener(_handleTabChange); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + context.read().requestMicrophoneListRefresh( + notifyIfUnchanged: true, + ); + }); _subscription = eventBus.on().listen(( event, diff --git a/lib/modules/room/voice_room_route.dart b/lib/modules/room/voice_room_route.dart index c32720f..4eb9dc7 100644 --- a/lib/modules/room/voice_room_route.dart +++ b/lib/modules/room/voice_room_route.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:fluro/fluro.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:yumi/modules/room/background/room_background_preview_page.dart'; import 'package:yumi/modules/room/background/room_background_select_page.dart'; import 'package:yumi/modules/room/background/room_background_upload_page.dart'; import 'package:yumi/modules/room/edit/room_edit_page.dart'; @@ -15,6 +16,7 @@ class VoiceRoomRoute implements SCIRouterProvider { static String roomEdit = '/room/roomEdit'; static String roomTheme = '/room/roomTheme'; static String roomBackgroundSelect = '/room/background/select'; + static String roomBackgroundPreview = '/room/background/preview'; static String roomBackgroundUpload = '/room/background/upload'; static Route _buildRoute(Widget page) { @@ -68,6 +70,16 @@ class VoiceRoomRoute implements SCIRouterProvider { ).push(_buildRoute(const RoomBackgroundUploadPage())); } + static Future openRoomBackgroundPreview( + BuildContext context, { + String? backgroundPath, + bool rootNavigator = false, + }) { + return Navigator.of(context, rootNavigator: rootNavigator).push( + _buildRoute(RoomBackgroundPreviewPage(backgroundPath: backgroundPath)), + ); + } + @override void initRouter(FluroRouter router) { router.define( @@ -84,6 +96,12 @@ class VoiceRoomRoute implements SCIRouterProvider { handlerFunc: (_, params) => const RoomBackgroundSelectPage(), ), ); + router.define( + roomBackgroundPreview, + handler: Handler( + handlerFunc: (_, params) => const RoomBackgroundPreviewPage(), + ), + ); router.define( roomBackgroundUpload, handler: Handler( diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index 6857ddf..9a5953b 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -7,6 +7,7 @@ import 'package:yumi/app_localizations.dart'; import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/modules/store/store_route.dart'; import 'package:yumi/modules/user/settings/settings_route.dart'; +import 'package:yumi/modules/user/vip/vip_route.dart'; import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; @@ -79,7 +80,7 @@ class _MePage2State extends State { SizedBox(height: 12.w), _buildEntryRow(), SizedBox(height: 12.w), - _buildWalletCard(), + _buildWalletVipRow(), SizedBox(height: 12.w), _buildMenuCard2(), SizedBox(height: 12.w), @@ -233,12 +234,21 @@ class _MePage2State extends State { ); } + Widget _buildWalletVipRow() { + return Row( + children: [ + Expanded(child: _buildVipEntryCard()), + SizedBox(width: 10.w), + Expanded(child: _buildWalletCard()), + ], + ); + } + Widget _buildWalletCard() { return SCDebounceWidget( onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge), child: Container( - width: double.infinity, - height: 90.w, + height: 68.w, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.w), image: const DecorationImage( @@ -246,44 +256,53 @@ class _MePage2State extends State { fit: BoxFit.fill, ), ), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ Text( SCAppLocalizations.of(context)!.wallet, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 24.sp, + fontSize: 18.sp, fontWeight: FontWeight.w800, fontStyle: FontStyle.italic, color: Colors.white, + height: 1, ), ), - SizedBox(height: 4.w), + SizedBox(height: 2.w), Row( children: [ Image.asset( - 'sc_images/general/sc_icon_jb.png', - width: 28.w, - height: 28.w, + 'sc_images/general/sc_icon_wallet_coin.png', + width: 18.w, + height: 18.w, ), - SizedBox(width: 6.w), - Consumer( - builder: (context, ref, child) { - return Text( - ':${_balanceText(ref.myBalance)}', - style: TextStyle( - color: Colors.white, - fontSize: 24.sp, - fontWeight: FontWeight.w700, - fontStyle: FontStyle.italic, - ), - ); - }, + SizedBox(width: 4.w), + Expanded( + child: Consumer( + builder: (context, ref, child) { + return Text( + ':${_balanceText(ref.myBalance)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + height: 1, + ), + ); + }, + ), ), ], ), @@ -292,8 +311,8 @@ class _MePage2State extends State { ), Image.asset( 'sc_images/index/sc_icon_wallet_icon.png', - width: 68.w, - height: 68.w, + width: 40.w, + height: 40.w, ), ], ), @@ -301,6 +320,103 @@ class _MePage2State extends State { ); } + Widget _buildVipEntryCard() { + return SCDebounceWidget( + onTap: () => SCNavigatorUtils.push(context, VipRoute.detail), + child: Container( + height: 68.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all(color: const Color(0xFFFFD37A), width: 1.w), + image: const DecorationImage( + image: AssetImage('sc_images/vip/sc_vip_entry_bg.png'), + fit: BoxFit.fill, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 10.w, + offset: Offset(0, 4.w), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + PositionedDirectional( + top: 12.w, + start: 14.w, + child: Text( + 'VIP', + style: TextStyle( + color: Colors.white, + fontFamily: 'Source Han Sans SC', + fontSize: 16.sp, + fontWeight: FontWeight.w700, + height: 1, + ), + ), + ), + PositionedDirectional( + start: 13.w, + bottom: 10.w, + child: _buildVipLevelMark(5), + ), + PositionedDirectional( + end: 2.w, + top: 8.w, + child: Image.asset( + 'sc_images/vip/sc_vip_badge_5.png', + width: 52.w, + height: 52.w, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ); + } + + Widget _buildVipLevelMark(int level) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'sc_images/vip/sc_vip_letter_v.png', + width: 18.1.w, + height: 15.3.w, + fit: BoxFit.contain, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_i.png', + width: 8.3.w, + height: 15.3.w, + fit: BoxFit.contain, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_p.png', + width: 18.1.w, + height: 15.3.w, + fit: BoxFit.contain, + ), + SizedBox(width: 3.w), + _buildVipLevelNumber(level), + ], + ); + } + + Widget _buildVipLevelNumber(int level) { + return Image.asset( + 'sc_images/vip/sc_vip_number_${level}_filled.png', + height: 15.247.w, + fit: BoxFit.fill, + color: const Color(0xFFFFD155), + colorBlendMode: BlendMode.srcIn, + ); + } + Widget _buildMenuCard2() { final userProfile = context.watch(); final items = <_MenuRowData>[]; @@ -343,7 +459,7 @@ class _MePage2State extends State { if (userProfile.userIdentity?.bdLeader ?? false) { items.add( _MenuRowData( - title: SCAppLocalizations.of(context)!.bdLeader, + title: SCAppLocalizations.of(context)!.admin, assetIcon: 'sc_images/index/sc_icon_bd_leader.png', onTap: () { SCNavigatorUtils.push( diff --git a/lib/modules/user/vip/vip_benefit_page.dart b/lib/modules/user/vip/vip_benefit_page.dart new file mode 100644 index 0000000..d29c7a9 --- /dev/null +++ b/lib/modules/user/vip/vip_benefit_page.dart @@ -0,0 +1,392 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class VipBenefitPage extends StatefulWidget { + const VipBenefitPage({super.key, this.initialIndex = 0}); + + final int initialIndex; + + @override + State createState() => _VipBenefitPageState(); +} + +class _VipBenefitPageState extends State { + static const List<_VipBenefitData> _benefits = [ + _VipBenefitData( + title: 'Gif Profile Picture', + description: 'You can upload a Gif image as your profile picture', + icon: Icons.image_outlined, + ), + _VipBenefitData( + title: 'Colorful Nickname', + description: 'Your nickname can use a special colorful display effect', + icon: Icons.notes_rounded, + ), + _VipBenefitData( + title: 'Gif Room Picture', + description: 'You can use a Gif image as your room cover picture', + icon: Icons.home_rounded, + ), + _VipBenefitData( + title: 'Colorful ID', + description: 'Your user ID can use a highlighted colorful style', + icon: Icons.pin_rounded, + isIdIcon: true, + ), + ]; + + late int _selectedIndex; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.initialIndex.clamp(0, _benefits.length - 1).toInt(); + } + + @override + Widget build(BuildContext context) { + final selected = _benefits[_selectedIndex]; + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)], + stops: const [0, 0.40], + ), + ), + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: Image.asset( + 'sc_images/vip/sc_vip_page_bg.png', + width: ScreenUtil().screenWidth, + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + height: 300.w, + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.36), + Colors.black.withValues(alpha: 0.62), + Colors.transparent, + ], + stops: const [0, 0.52, 1], + ), + ), + ), + ), + ), + SafeArea( + bottom: false, + child: Column( + children: [ + _buildTopBar(context), + SizedBox(height: 18.w), + _buildBenefitTabs(), + SizedBox(height: 36.w), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(36.w, 0, 36.w, 32.w), + child: _buildBenefitCard(selected), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTopBar(BuildContext context) { + return SizedBox( + height: 44.w, + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => Navigator.pop(context), + child: SizedBox( + width: 44.w, + height: 44.w, + child: Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + color: Colors.white, + size: 28.w, + ), + ), + ), + ), + Text( + 'VIP Benefits', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildBenefitTabs() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (int index = 0; index < _benefits.length; index++) + _buildBenefitTab(index, _benefits[index]), + ], + ); + } + + Widget _buildBenefitTab(int index, _VipBenefitData item) { + final selected = index == _selectedIndex; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _selectedIndex = index), + child: SizedBox( + width: 58.w, + child: Column( + children: [ + Container( + width: 54.w, + height: 54.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + selected + ? Colors.white.withValues(alpha: 0.08) + : Colors.transparent, + ), + alignment: Alignment.center, + child: _buildBenefitIcon(item, size: 28.w), + ), + SizedBox(height: 8.w), + AnimatedOpacity( + duration: const Duration(milliseconds: 160), + opacity: selected ? 1 : 0, + child: Container( + width: 20.w, + height: 3.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(999.w), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBenefitCard(_VipBenefitData item) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFF171717), + borderRadius: BorderRadius.circular(12.w), + ), + padding: EdgeInsets.fromLTRB(14.w, 22.w, 14.w, 14.w), + child: Column( + children: [ + Text( + item.title, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 14.w), + Text( + item.description, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.58), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + height: 1.22, + ), + ), + SizedBox(height: 18.w), + _buildPreviewPlaceholder(item), + ], + ), + ); + } + + Widget _buildPreviewPlaceholder(_VipBenefitData item) { + return Container( + width: double.infinity, + height: 260.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: const [Color(0xFF234F73), Color(0xFFF5F5F5)], + stops: const [0, 0.58], + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + left: 18.w, + top: 16.w, + child: Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + color: Colors.white, + size: 24.w, + ), + ), + Positioned( + top: 80.w, + child: Container( + width: 64.w, + height: 64.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all(color: Colors.white, width: 2.w), + ), + alignment: Alignment.center, + child: _buildBenefitIcon(item, size: 34.w), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 110.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18.w), + topRight: Radius.circular(18.w), + bottomLeft: Radius.circular(12.w), + bottomRight: Radius.circular(12.w), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + item.title, + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 17.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 10.w), + Container( + width: 88.w, + height: 18.w, + decoration: BoxDecoration( + color: const Color(0xFFF0F0F0), + borderRadius: BorderRadius.circular(9.w), + ), + alignment: Alignment.center, + child: Text( + 'Placeholder', + style: TextStyle( + color: const Color(0xFF555555), + fontSize: 9.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildBenefitIcon(_VipBenefitData item, {required double size}) { + if (item.isIdIcon) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.w), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFF7244), Color(0xFFFFD84E)], + ), + ), + alignment: Alignment.center, + child: Text( + 'ID', + style: TextStyle( + color: Colors.white, + fontSize: (size * 0.42).sp, + fontWeight: FontWeight.w800, + height: 1, + ), + ), + ); + } + + return ShaderMask( + shaderCallback: + (bounds) => const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFF6F3D), Color(0xFFFFD84E)], + ).createShader(bounds), + blendMode: BlendMode.srcIn, + child: Icon(item.icon, color: Colors.white, size: size), + ); + } +} + +class _VipBenefitData { + const _VipBenefitData({ + required this.title, + required this.description, + required this.icon, + this.isIdIcon = false, + }); + + final String title; + final String description; + final IconData icon; + final bool isIdIcon; +} diff --git a/lib/modules/user/vip/vip_detail_page.dart b/lib/modules/user/vip/vip_detail_page.dart new file mode 100644 index 0000000..6360f4c --- /dev/null +++ b/lib/modules/user/vip/vip_detail_page.dart @@ -0,0 +1,786 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/user/vip/vip_route.dart'; + +class VipDetailPage extends StatefulWidget { + const VipDetailPage({super.key}); + + @override + State createState() => _VipDetailPageState(); +} + +class _VipDetailPageState extends State { + int _selectedLevel = 1; + late bool _showUpdateButton; + + @override + void initState() { + super.initState(); + _showUpdateButton = false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)], + stops: const [0, 0.40], + ), + ), + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: Image.asset( + 'sc_images/vip/sc_vip_page_bg.png', + width: ScreenUtil().screenWidth, + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + height: 300.w, + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.36), + Colors.black.withValues(alpha: 0.62), + Colors.transparent, + ], + stops: const [0, 0.52, 1], + ), + ), + ), + ), + ), + SafeArea( + bottom: false, + child: Column( + children: [ + _buildTopBar(), + _buildLevelTabs(), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(12.w, 40.w, 12.w, 18.w), + child: Column( + children: [ + _buildLevelCard(), + SizedBox(height: 30.w), + _buildLevelDivider(), + SizedBox(height: 3.w), + _buildSectionTitle('Decoration Privileges'), + SizedBox(height: 10.w), + _buildDecorationGrid(), + SizedBox(height: 20.w), + _buildSectionTitle('VIP Privileges'), + SizedBox(height: 14.w), + _buildPrivilegeGrid(), + SizedBox(height: 76.w), + ], + ), + ), + ), + _buildActivateBar(), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTopBar() { + return SizedBox( + height: 44.w, + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => Navigator.pop(context), + child: SizedBox( + width: 44.w, + height: 44.w, + child: Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + color: Colors.white, + size: 28.w, + ), + ), + ), + ), + Text( + 'VIP', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + Align( + alignment: AlignmentDirectional.centerEnd, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => SCNavigatorUtils.push(context, VipRoute.instruction), + child: Padding( + padding: EdgeInsetsDirectional.only(end: 12.w), + child: Image.asset( + 'sc_images/vip/sc_vip_question.png', + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildLevelTabs() { + return SizedBox( + height: 43.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 12.w), + itemCount: 5, + separatorBuilder: (_, __) => SizedBox(width: 30.w), + itemBuilder: (context, index) { + final level = index + 1; + final selected = level == _selectedLevel; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _selectedLevel = level), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 4.w), + Text( + 'VIP$level', + style: TextStyle( + color: + selected + ? Colors.white + : Colors.white.withValues(alpha: 0.42), + fontSize: 15.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 6.w), + SizedBox( + width: 0, + height: 9.5.w, + child: OverflowBox( + minWidth: 60.w, + maxWidth: 60.w, + alignment: Alignment.center, + child: + selected + ? Image.asset( + 'sc_images/vip/sc_vip_selected_line.png', + width: 60.w, + height: 9.5.w, + fit: BoxFit.fill, + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildLevelCard() { + final assetLevel = _assetLevel; + return Center( + child: SizedBox( + width: 351.w, + height: 124.5.w, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + color: const Color(0xFFEBCB76), + width: 1.w, + ), + image: DecorationImage( + image: AssetImage( + 'sc_images/vip/sc_vip_card_bg_$assetLevel.png', + ), + fit: BoxFit.fill, + ), + ), + ), + ), + PositionedDirectional( + start: 20.w, + top: 33.w, + child: _buildVipLevelMark(_selectedLevel, scale: 1.18), + ), + PositionedDirectional( + start: 22.w, + top: 80.w, + child: Text( + 'Not activated', + style: TextStyle( + color: const Color(0xFFF2C766).withValues(alpha: 0.78), + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + PositionedDirectional( + end: 24.w, + top: -36.w, + child: Image.asset( + 'sc_images/vip/sc_vip_badge_$assetLevel.png', + width: 135.w, + height: 135.w, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLevelDivider() { + final ornamentLayout = _ornamentLayoutForLevel(_assetLevel); + return SizedBox( + height: 38.w, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Positioned( + left: -12.w, + right: -12.w, + top: 0, + height: 8.w, + child: Image.asset( + 'sc_images/vip/sc_vip_top_border_$_assetLevel.png', + fit: BoxFit.fill, + ), + ), + Positioned( + left: 0, + right: 0, + top: ornamentLayout.top.w, + child: Transform.translate( + offset: Offset(ornamentLayout.dx.w, 0), + child: Center( + child: Image.asset( + 'sc_images/vip/sc_vip_card_ornament_$_assetLevel.png', + width: ornamentLayout.width.w, + height: ornamentLayout.height.w, + fit: BoxFit.contain, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'sc_images/vip/sc_vip_title_left.png', + width: 16.w, + height: 20.w, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8.w), + Image.asset( + 'sc_images/vip/sc_vip_title_right.png', + width: 16.w, + height: 20.w, + fit: BoxFit.contain, + ), + ], + ); + } + + Widget _buildDecorationGrid() { + final items = [ + _VipFeatureData( + 'SVIP Badge', + 'sc_images/vip/sc_vip_badge_$_assetLevel.png', + ), + _VipFeatureData('Frame', null, Icons.person_pin_circle_outlined), + _VipFeatureData('Mount', null, Icons.auto_awesome_motion), + _VipFeatureData('Mic Animation', null, Icons.radio_button_checked), + _VipFeatureData('Entry Effect', null, Icons.airline_seat_recline_extra), + ]; + return Wrap( + spacing: 10.w, + runSpacing: 10.w, + children: + items + .map( + (item) => _buildFeatureTile( + item, + width: item.title == 'Entry Effect' ? 206.w : 98.w, + height: 100.w, + ), + ) + .toList(), + ); + } + + Widget _buildPrivilegeGrid() { + final items = [ + _VipFeatureData('Gif Profile\nPicture', null, Icons.badge_outlined), + _VipFeatureData('Colorful\nNickname', null, Icons.credit_card), + _VipFeatureData('Gif Room\nPicture', null, Icons.home_rounded), + _VipFeatureData('Colorful ID', null, Icons.pin_rounded), + ]; + return Wrap( + spacing: 34.w, + runSpacing: 24.w, + children: + items.asMap().entries.map((entry) { + final item = entry.value; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: + () => SCNavigatorUtils.push( + context, + '${VipRoute.benefit}?index=${entry.key}', + ), + child: SizedBox( + width: 74.w, + child: Column( + children: [ + Container( + width: 46.w, + height: 46.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.08), + ), + alignment: Alignment.center, + child: Icon( + item.icon, + color: const Color(0xFFFF8E3D), + size: 23.w, + ), + ), + SizedBox(height: 7.w), + Text( + item.title, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w400, + height: 1.1, + ), + ), + ], + ), + ), + ); + }).toList(), + ); + } + + Widget _buildRewardPlaceholder(IconData icon) { + return Center( + child: Container( + width: 46.w, + height: 46.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + const Color(0xFFFFD155).withValues(alpha: 0.28), + Colors.white.withValues(alpha: 0.06), + ], + ), + border: Border.all( + color: const Color(0xFFFFD155).withValues(alpha: 0.45), + width: 0.8.w, + ), + ), + alignment: Alignment.center, + child: Icon(icon, color: const Color(0xFFFFD155), size: 24.w), + ), + ); + } + + Widget _buildWideRewardPlaceholder() { + return Container( + width: double.infinity, + height: 40.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.w), + gradient: LinearGradient( + colors: [ + const Color(0xFFFFD155).withValues(alpha: 0.14), + const Color(0xFF7F4DFF).withValues(alpha: 0.16), + const Color(0xFFFFD155).withValues(alpha: 0.14), + ], + ), + border: Border.all( + color: const Color(0xFFFFD155).withValues(alpha: 0.35), + width: 0.8.w, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + for (int i = 0; i < 5; i++) + Container( + width: 4.w, + height: 4.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.45), + ), + ), + ], + ), + ); + } + + Widget _buildFeatureTile( + _VipFeatureData item, { + required double width, + required double height, + }) { + final isWide = item.title == 'Entry Effect'; + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.w), + border: Border.all(color: const Color(0xFFEBCB76), width: 1.w), + color: Colors.black.withValues(alpha: 0.38), + ), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (item.assetPath != null) + Expanded(child: Image.asset(item.assetPath!, fit: BoxFit.contain)) + else + Expanded( + child: + isWide + ? Center(child: _buildWideRewardPlaceholder()) + : _buildRewardPlaceholder( + item.icon ?? Icons.auto_awesome, + ), + ), + SizedBox(height: 5.w), + Text( + item.title, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } + + Widget _buildActivateBar() { + final bottomInset = MediaQuery.of(context).padding.bottom; + final actionButton = _actionButtonConfigForLevel(_assetLevel); + final barOverflow = 20.w + bottomInset; + return SizedBox( + height: 106.5.w, + width: double.infinity, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 0, + right: 0, + top: 0, + bottom: -barOverflow, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.w), + topRight: Radius.circular(20.w), + ), + child: Image.asset( + 'sc_images/vip/sc_vip_card_bg_$_assetLevel.png', + fit: BoxFit.fitWidth, + ), + ), + ), + + Positioned.fill( + child: Padding( + padding: EdgeInsets.fromLTRB( + 30.w, + 30.w, + 30.w, + 10.w + bottomInset, + ), + child: Row( + children: [ + Container( + width: 22.w, + height: 22.w, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFFFA629), + ), + alignment: Alignment.center, + child: Icon( + Icons.stars_rounded, + color: Colors.white, + size: 14.w, + ), + ), + SizedBox(width: 6.w), + Text( + '30.5 M / 30 Days', + style: TextStyle( + color: const Color(0xFFFFE8A7), + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + const Spacer(), + Container( + width: 106.w, + height: 42.w, + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(actionButton.backgroundAssetPath), + fit: BoxFit.fill, + ), + ), + child: Text( + actionButton.label, + textAlign: TextAlign.center, + style: TextStyle( + color: actionButton.textColor, + fontFamily: actionButton.fontFamily, + fontSize: actionButton.fontSize.sp, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w700, + height: 1, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildVipLevelMark(int level, {double scale = 1}) { + return Transform.scale( + scale: scale, + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'sc_images/vip/sc_vip_letter_v.png', + width: 28.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_i.png', + width: 14.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_p.png', + width: 28.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + SizedBox(width: 3.w), + _buildVipLevelNumber(level), + ], + ), + ); + } + + Widget _buildVipLevelNumber(int level) { + return Image.asset( + 'sc_images/vip/sc_vip_number_${level}_filled.png', + height: 25.w, + fit: BoxFit.fill, + color: const Color(0xFFFFD155), + colorBlendMode: BlendMode.srcIn, + ); + } + + _VipOrnamentLayout _ornamentLayoutForLevel(int level) { + switch (level) { + case 1: + return const _VipOrnamentLayout( + width: 176.5, + height: 36, + top: -10, + dx: 0, + ); + case 2: + return const _VipOrnamentLayout( + width: 246, + height: 61, + top: -20, + dx: 0, + ); + case 3: + return const _VipOrnamentLayout( + width: 277, + height: 79.5, + top: -35, + dx: 0, + ); + case 4: + return const _VipOrnamentLayout( + width: 240.5, + height: 79.5, + top: -35, + dx: 0, + ); + case 5: + default: + return const _VipOrnamentLayout( + width: 282.5, + height: 101.5, + top: -48, + dx: 0, + ); + } + } + + _VipActionButtonConfig _actionButtonConfigForLevel(int level) { + switch (level) { + case 1: + case 2: + case 3: + case 4: + case 5: + default: + return _showUpdateButton + ? const _VipActionButtonConfig.update() + : const _VipActionButtonConfig.activate(); + } + } + + int get _assetLevel => _selectedLevel.clamp(1, 5).toInt(); +} + +class _VipOrnamentLayout { + const _VipOrnamentLayout({ + required this.width, + required this.height, + required this.top, + required this.dx, + }); + + final double width; + final double height; + final double top; + final double dx; +} + +class _VipFeatureData { + const _VipFeatureData(this.title, this.assetPath, [this.icon]); + + final String title; + final String? assetPath; + final IconData? icon; +} + +class _VipActionButtonConfig { + const _VipActionButtonConfig({ + required this.label, + required this.backgroundAssetPath, + required this.textColor, + required this.fontSize, + required this.fontFamily, + }); + + const _VipActionButtonConfig.activate() + : this( + label: 'Activate', + backgroundAssetPath: 'sc_images/vip/sc_vip_activate_button_bg.png', + textColor: const Color(0xFF7B4B16), + fontSize: 14, + fontFamily: null, + ); + + const _VipActionButtonConfig.update() + : this( + label: 'Update', + backgroundAssetPath: 'sc_images/vip/sc_vip_update_button_bg.png', + textColor: const Color(0xFF740000), + fontSize: 16, + fontFamily: 'Source Han Sans SC', + ); + + final String label; + final String backgroundAssetPath; + final Color textColor; + final double fontSize; + final String? fontFamily; +} diff --git a/lib/modules/user/vip/vip_instruction_page.dart b/lib/modules/user/vip/vip_instruction_page.dart new file mode 100644 index 0000000..a38205e --- /dev/null +++ b/lib/modules/user/vip/vip_instruction_page.dart @@ -0,0 +1,491 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class VipInstructionPage extends StatefulWidget { + const VipInstructionPage({super.key}); + + @override + State createState() => _VipInstructionPageState(); +} + +class _VipInstructionPageState extends State { + _VipInstructionTab _selectedTab = _VipInstructionTab.record; + + static const List<_VipRecordRow> _recordRows = [ + _VipRecordRow( + level: 1, + validDays: '30days', + type: 'Expiration', + time: '21/09/2029\n12:99:00', + ), + _VipRecordRow( + level: 2, + validDays: '10days', + type: 'Card', + time: '21/09/2029\n12:99:00', + ), + _VipRecordRow( + level: 3, + validDays: '30days', + type: 'Renew', + time: '21/09/2029\n12:99:00', + ), + _VipRecordRow( + level: 4, + validDays: '30days', + type: 'Purchase', + time: '21/09/2029\n12:99:00', + ), + _VipRecordRow( + level: 5, + validDays: '30days', + type: 'Upgrade', + time: '21/09/2029\n12:99:00', + ), + ]; + + static const List<_VipDescriptionSection> _descriptionSections = [ + _VipDescriptionSection( + 'Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.', + ), + _VipDescriptionSection( + 'You can upgrade to a higher VIP level by paying the price difference. After upgrading, a new 30-day validity period will be given.', + bullets: [ + 'Price Difference = Target Price - Remaining Price for Current Days.', + 'The upgrade takes effect immediately, and the remaining time of the previous level will be overwritten.', + ], + ), + _VipDescriptionSection( + 'Once a higher-level VIP is activated, it is not possible to purchase a lower-level VIP.', + ), + _VipDescriptionSection( + 'Different levels cannot be stacked simultaneously; only one level can be activated at a time.', + ), + _VipDescriptionSection( + 'If using a card from your inventory to activate a new level:', + bullets: [ + 'If the new level is higher than the current level, it will directly upgrade, and the remaining time of the previous level will be cleared.', + 'If the new level is lower than the current level, activation cannot be used.', + ], + ), + _VipDescriptionSection( + 'After the VIP expires, the corresponding privileges will be lost, but the purchase records will not be cleared.', + ), + _VipDescriptionSection( + 'The platform reserves the final right to interpret the VIP rules.', + ), + ]; + + static const Color _tableLightColor = Color(0xFF262626); + static const Color _tableDarkColor = Color(0xFF191919); + static const Color _tableDividerColor = Color(0xFF333333); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)], + stops: const [0, 0.40], + ), + ), + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: Image.asset( + 'sc_images/vip/sc_vip_page_bg.png', + width: ScreenUtil().screenWidth, + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + height: 300.w, + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.36), + Colors.black.withValues(alpha: 0.62), + Colors.transparent, + ], + stops: const [0, 0.52, 1], + ), + ), + ), + ), + ), + SafeArea( + bottom: false, + child: Column( + children: [ + _buildTopBar(context), + SizedBox(height: 6.w), + _buildTabs(), + Expanded( + child: + _selectedTab == _VipInstructionTab.record + ? _buildRecordView() + : _buildDescriptionView(), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTopBar(BuildContext context) { + return SizedBox( + height: 44.w, + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => Navigator.pop(context), + child: SizedBox( + width: 44.w, + height: 44.w, + child: Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + color: Colors.white, + size: 28.w, + ), + ), + ), + ), + Text( + 'VIP Rules', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildTabs() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildTab(_VipInstructionTab.record, 'Record'), + SizedBox(width: 45.5.w), + _buildTab(_VipInstructionTab.description, 'Description'), + ], + ); + } + + Widget _buildTab(_VipInstructionTab tab, String title) { + final selected = _selectedTab == tab; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _selectedTab = tab), + child: Column( + children: [ + Text( + title, + style: TextStyle( + color: + selected ? Colors.white : Colors.white.withValues(alpha: 0.5), + fontFamily: 'Source Han Sans SC', + fontSize: 18.sp, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w400, + height: 1, + ), + ), + SizedBox(height: 8.w), + AnimatedOpacity( + duration: const Duration(milliseconds: 160), + opacity: selected ? 1 : 0, + child: Container( + width: 18.w, + height: 4.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(999.w), + ), + ), + ), + ], + ), + ); + } + + Widget _buildRecordView() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 24.w), + child: _buildRecordTable(), + ); + } + + Widget _buildRecordTable() { + return ClipRRect( + borderRadius: BorderRadius.circular(12.w), + child: Container( + color: _tableDarkColor, + child: Column( + children: [ + _buildTableRow( + const ['Level', 'Valid Days', 'type', 'Time'], + height: 38.5.w, + isHeader: true, + isLastRow: false, + ), + for (int i = 0; i < _recordRows.length; i++) + _buildRecordDataRow( + _recordRows[i], + isLastRow: i == _recordRows.length - 1, + ), + ], + ), + ), + ); + } + + Widget _buildTableRow( + List values, { + required double height, + required bool isHeader, + required bool isLastRow, + }) { + return SizedBox( + height: height, + child: Row( + children: List.generate(values.length, (index) { + return _buildTableCell( + columnIndex: index, + isLastColumn: index == values.length - 1, + isLastRow: isLastRow, + flex: _columnFlex(index), + child: Text( + values[index], + textAlign: TextAlign.center, + style: _tableTextStyle, + ), + ); + }), + ), + ); + } + + Widget _buildRecordDataRow(_VipRecordRow row, {required bool isLastRow}) { + return SizedBox( + height: 76.w, + child: Row( + children: [ + _buildTableCell( + columnIndex: 0, + isLastColumn: false, + isLastRow: isLastRow, + flex: _columnFlex(0), + child: Image.asset( + 'sc_images/vip/sc_vip_badge_${row.level}.png', + width: 60.w, + height: 60.w, + fit: BoxFit.contain, + ), + ), + _buildTableCell( + columnIndex: 1, + isLastColumn: false, + isLastRow: isLastRow, + flex: _columnFlex(1), + child: Text( + row.validDays, + textAlign: TextAlign.center, + style: _tableTextStyle, + ), + ), + _buildTableCell( + columnIndex: 2, + isLastColumn: false, + isLastRow: isLastRow, + flex: _columnFlex(2), + child: Text( + row.type, + textAlign: TextAlign.center, + style: _tableTextStyle, + ), + ), + _buildTableCell( + columnIndex: 3, + isLastColumn: true, + isLastRow: isLastRow, + flex: _columnFlex(3), + child: Text( + row.time, + textAlign: TextAlign.center, + style: _tableTextStyle, + ), + ), + ], + ), + ); + } + + Widget _buildTableCell({ + required int columnIndex, + required bool isLastColumn, + required bool isLastRow, + required int flex, + required Widget child, + }) { + return Expanded( + flex: flex, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w), + decoration: BoxDecoration( + color: columnIndex.isEven ? _tableLightColor : _tableDarkColor, + border: Border( + right: + isLastColumn + ? BorderSide.none + : BorderSide(color: _tableDividerColor, width: 1.w), + bottom: + isLastRow + ? BorderSide.none + : BorderSide(color: _tableDividerColor, width: 1.w), + ), + ), + child: child, + ), + ); + } + + Widget _buildDescriptionView() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 32.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < _descriptionSections.length; i++) ...[ + _buildDescriptionSection(i + 1, _descriptionSections[i]), + SizedBox(height: 18.w), + ], + ], + ), + ); + } + + Widget _buildDescriptionSection(int index, _VipDescriptionSection section) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$index. ${section.text}', style: _descriptionTextStyle), + if (section.bullets.isNotEmpty) ...[ + SizedBox(height: 8.w), + for (final bullet in section.bullets) ...[ + Padding( + padding: EdgeInsetsDirectional.only(start: 18.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('•', style: _descriptionHighlightStyle), + SizedBox(width: 8.w), + Expanded( + child: Text(bullet, style: _descriptionHighlightStyle), + ), + ], + ), + ), + SizedBox(height: 4.w), + ], + ], + ], + ); + } + + int _columnFlex(int index) { + switch (index) { + case 0: + return 92; + case 1: + return 100; + case 2: + return 114; + case 3: + default: + return 110; + } + } + + TextStyle get _tableTextStyle => TextStyle( + color: Colors.white, + fontFamily: 'Source Han Sans SC', + fontSize: 12.sp, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w400, + height: 14.5 / 12, + ); + + TextStyle get _descriptionTextStyle => TextStyle( + color: Colors.white, + fontFamily: 'Source Han Sans SC', + fontSize: 16.sp, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w400, + height: 14.5 / 12, + ); + + TextStyle get _descriptionHighlightStyle => TextStyle( + color: const Color(0xFFFF9830), + fontFamily: 'Source Han Sans SC', + fontSize: 16.sp, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w400, + height: 14.5 / 12, + ); +} + +enum _VipInstructionTab { record, description } + +class _VipRecordRow { + const _VipRecordRow({ + required this.level, + required this.validDays, + required this.type, + required this.time, + }); + + final int level; + final String validDays; + final String type; + final String time; +} + +class _VipDescriptionSection { + const _VipDescriptionSection(this.text, {this.bullets = const []}); + + final String text; + final List bullets; +} diff --git a/lib/modules/user/vip/vip_route.dart b/lib/modules/user/vip/vip_route.dart new file mode 100644 index 0000000..5bbc765 --- /dev/null +++ b/lib/modules/user/vip/vip_route.dart @@ -0,0 +1,32 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/user/vip/vip_benefit_page.dart'; +import 'package:yumi/modules/user/vip/vip_detail_page.dart'; +import 'package:yumi/modules/user/vip/vip_instruction_page.dart'; + +class VipRoute implements SCIRouterProvider { + static String detail = '/main/me/vip/detail'; + static String instruction = '/main/me/vip/instruction'; + static String benefit = '/main/me/vip/benefit'; + + @override + void initRouter(FluroRouter router) { + router.define( + detail, + handler: Handler(handlerFunc: (_, params) => const VipDetailPage()), + ); + router.define( + instruction, + handler: Handler(handlerFunc: (_, params) => const VipInstructionPage()), + ); + router.define( + benefit, + handler: Handler( + handlerFunc: + (_, params) => VipBenefitPage( + initialIndex: int.tryParse(params['index']?.first ?? '') ?? 0, + ), + ), + ); + } +} diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index c0405a4..ff108d9 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -63,6 +63,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool _isRefreshingOnlineUsers = false; bool _disableMicListRefreshForCurrentSession = false; bool _disableOnlineUsersRefreshForCurrentSession = false; + bool _isSelfMicActionInFlight = false; bool _pendingMicListRefresh = false; Timer? _deferredMicListRefreshTimer; int _lastMicListRefreshStartedAtMs = 0; @@ -192,6 +193,17 @@ class RealTimeCommunicationManager extends ChangeNotifier { return error.toString(); } + bool _isCancelledRequest(Object error) { + if (error is DioException) { + return error.type == DioExceptionType.cancel || + (error.error?.toString().trim().toLowerCase() == 'cancel'); + } + final normalizedMessage = error.toString().toLowerCase(); + return normalizedMessage.contains('dioexceptiontype.cancel') || + normalizedMessage.contains('error: cancel') || + normalizedMessage.contains('request was manually cancelled'); + } + bool _sameOnlineUsers( List previous, List next, @@ -1364,6 +1376,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { _resetLocalAudioRuntimeTracking(); _disableMicListRefreshForCurrentSession = false; _disableOnlineUsersRefreshForCurrentSession = false; + _isSelfMicActionInFlight = false; roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = @@ -1426,7 +1439,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } } else { - SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user; + SocialChatUserProfile? roomWheatUser = micAtIndexForDisplay(index)?.user; showBottomInBottomDialog( context!, EmptyMaiSelect(index: index, clickUser: roomWheatUser), @@ -1474,7 +1487,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { return false; } - final seat = roomWheatMap[index]; + final seat = micAtIndexForDisplay(index); final seatUser = seat?.user; if (seatUser == null) { if (!(seat?.micLock ?? false) && !isRoomAdmin) { @@ -1550,6 +1563,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } + if (_isSelfMicActionInFlight) { + return; + } + _isSelfMicActionInFlight = true; try { var micGoUpRes = await SCChatRoomRepository().micGoUp( currenRoom?.roomProfile?.roomProfile?.id ?? "", @@ -1608,6 +1625,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { roomToken: micGoUpRes.roomToken, ); } + _syncSelfMicRuntimeState(); if (roomWheatMap[targetIndex]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { @@ -1618,11 +1636,21 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); _refreshMicListSilently(); } catch (ex) { - SCTts.show('Failed to put on the microphone, $ex'); + if (_isCancelledRequest(ex)) { + _refreshMicListSilently(); + return; + } + SCTts.show('Failed to put on the microphone, ${_errorMessageFrom(ex)}'); + } finally { + _isSelfMicActionInFlight = false; } } xiaMai(num index) async { + if (_isSelfMicActionInFlight) { + return; + } + _isSelfMicActionInFlight = true; try { await SCChatRoomRepository().micGoDown( currenRoom?.roomProfile?.roomProfile?.id ?? "", @@ -1648,13 +1676,20 @@ class RealTimeCommunicationManager extends ChangeNotifier { engine?.renewToken(""); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); _clearUserFromSeats(currentUserId); + _syncSelfMicRuntimeState(); notifyListeners(); requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } catch (ex) { - SCTts.show('Failed to leave the microphone, $ex'); + if (_isCancelledRequest(ex)) { + _refreshMicListSilently(); + return; + } + SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}'); + } finally { + _isSelfMicActionInFlight = false; } } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 5b83a97..4215263 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -916,7 +916,8 @@ class RealTimeMessagingManager extends ChangeNotifier { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); - } else if (msg.type == SCRoomMsgType.image) { + } else if (msg.type == SCRoomMsgType.image || + (msg.type == SCRoomMsgType.emoticons && (msg.number ?? -1) < 0)) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { print('大于200条消息'); @@ -1431,11 +1432,13 @@ class RealTimeMessagingManager extends ChangeNotifier { return; } if (msg.type == SCRoomMsgType.emoticons) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - return; + if ((msg.number ?? -1) >= 0) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + return; + } } if (msg.type == SCRoomMsgType.micChange) { Provider.of( diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index 136e00e..1e43a97 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -44,7 +44,7 @@ abstract class SocialChatUserRepository { String openId, SCUserProfileCmd userProfileCmd, { SCMobileAuthCmd? mobileAuthCmd, - String invitePeople, + String invitationCode, }); ///关注的房间 diff --git a/lib/shared/data_sources/sources/remote/net/api.dart b/lib/shared/data_sources/sources/remote/net/api.dart index 2756327..a8b3dd6 100644 --- a/lib/shared/data_sources/sources/remote/net/api.dart +++ b/lib/shared/data_sources/sources/remote/net/api.dart @@ -340,8 +340,13 @@ class BaseNetworkClient { response: e.response, error: '服务器错误: ${e.response?.data["errorCode"]}', ); - case DioExceptionType.cancel: - return DioException(requestOptions: e.requestOptions, error: 'Cancel'); + case DioExceptionType.cancel: + return DioException( + requestOptions: e.requestOptions, + response: e.response, + type: DioExceptionType.cancel, + error: e.error ?? 'Cancel', + ); default: return DioException( requestOptions: e.requestOptions, 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 ad6b346..7a69592 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 @@ -60,18 +60,16 @@ class SCAccountRepository implements SocialChatUserRepository { String openId, SCUserProfileCmd userProfileCmd, { SCMobileAuthCmd? mobileAuthCmd, - String invitePeople = "", + String invitationCode = "", }) async { var param = {}; param["type"] = type; param["openId"] = openId; param["profile"] = userProfileCmd.toJson(); + param["invitationCode"] = invitationCode; if (mobileAuthCmd != null) { param["mobile"] = mobileAuthCmd.toJson(); } - if (invitePeople.isNotEmpty) { - param["invitePeople"] = invitePeople; - } final result = await http.post( "aa35848b37e5ba74d93e5a1f719c579cb289b5940c15040473b6f57550de35df", data: param, diff --git a/lib/ui_kit/widgets/room/bottom/room_bottom_chat_entry.dart b/lib/ui_kit/widgets/room/bottom/room_bottom_chat_entry.dart index 7aa5493..bcc82a3 100644 --- a/lib/ui_kit/widgets/room/bottom/room_bottom_chat_entry.dart +++ b/lib/ui_kit/widgets/room/bottom/room_bottom_chat_entry.dart @@ -40,30 +40,20 @@ class RoomBottomChatEntry extends StatelessWidget { ], ), child: Padding( - padding: EdgeInsetsDirectional.only(start: 14.w, end: 14.w), - child: Row( - children: [ - Image.asset( - 'sc_images/room/icon_room_input_t.png', - width: 22.w, - height: 22.w, - fit: BoxFit.contain, + padding: EdgeInsetsDirectional.only(start: 16.w, end: 16.w), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + localizations?.translate('roomBottomGreeting') ?? 'Hi...', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w700, + height: 1.0, ), - SizedBox(width: 10.w), - Expanded( - child: Text( - localizations?.translate('roomBottomGreeting') ?? 'Hi...', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.w700, - height: 1.0, - ), - ), - ), - ], + ), ), ), ), diff --git a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart index 68858da..c71cff2 100644 --- a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart +++ b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart @@ -1,9 +1,14 @@ -import 'package:flutter/cupertino.dart'; +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svga/flutter_svga.dart'; import 'package:provider/provider.dart'; import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; class LuckGiftNomorAnimWidget extends StatefulWidget { @@ -18,22 +23,9 @@ class LuckGiftNomorAnimWidget extends StatefulWidget { } class _LuckGiftNomorAnimWidgetState extends State { - static const double _groupShiftXRatio = 26 / 422; - static const double _groupShiftYRatio = 18 / 422; - static const double _groupShiftYExtraUnits = 40; - static const double _avatarScale = 0.8; - static const double _avatarExtraShiftXUnits = -2; - static const double _avatarExtraShiftYUnits = 18; - static const double _avatarDiameterRatio = 76 / 422; - static const double _avatarLeftRatio = 152 / 422; - static const double _avatarTopRatio = 64 / 422; - static const double _awardTopRatio = 153 / 422; - static const double _awardXOffsetRatio = -13 / 422; - static const double _multipleTopRatio = 193 / 422; - static const double _multipleXOffsetRatio = -22 / 422; - - String? _currentBurstEventId; - bool _isRewardAmountVisible = false; + static const String _avatarDynamicKey = "avatar"; + static const String _awardAmountDynamicKey = "digit01"; + static const String _multipleDynamicKey = "digit02"; bool _shouldPlayRewardBurst(Data rewardData) { return RealTimeMessagingManager.shouldPlayLuckyGiftBurst(rewardData); @@ -56,38 +48,201 @@ class _LuckGiftNomorAnimWidgetState extends State { return multiple.toString(); } - Widget _buildSenderAvatar({ - required String avatarUrl, - required double outerSize, - }) { - final avatarPadding = outerSize * 0.045; - final innerSize = outerSize - avatarPadding * 2; - return RepaintBoundary( - child: Container( - width: outerSize, - height: outerSize, - padding: EdgeInsets.all(avatarPadding), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFFFD680), - boxShadow: const [ - BoxShadow( - color: Color(0x55243A74), - blurRadius: 16, - offset: Offset(0, 6), + Future _configureRewardBurstSvga( + MovieEntity movieEntity, + Data rewardData, + ) async { + final avatarUrl = (rewardData.userAvatar ?? '').trim(); + if (avatarUrl.isNotEmpty) { + try { + final avatarImage = await _buildAvatarLayerImage(avatarUrl); + if (avatarImage != null) { + movieEntity.dynamicItem.setImage(avatarImage, _avatarDynamicKey); + } + } catch (error) { + debugPrint('[LuckGiftBurstSVGA] avatar sync failed: $error'); + } + } + + try { + final awardAmountImage = await _buildTextLayerImage( + text: _formatAwardAmount(rewardData.awardAmount ?? 0), + logicalWidth: 140, + logicalHeight: 65, + fontSize: 42, + color: const Color(0xFFFFF3B6), + shadowColor: const Color(0xCC7A3E00), + ); + movieEntity.dynamicItem.setImage( + awardAmountImage, + _awardAmountDynamicKey, + ); + } catch (error) { + debugPrint('[LuckGiftBurstSVGA] award amount sync failed: $error'); + } + + final multiple = rewardData.multiple ?? 0; + if (multiple > 0) { + try { + final multipleImage = await _buildTextLayerImage( + text: 'x${_formatMultiple(multiple)}', + logicalWidth: 166, + logicalHeight: 37, + fontSize: 28, + color: const Color(0xFFF4F6FF), + shadowColor: const Color(0x990B2D72), + ); + movieEntity.dynamicItem.setImage(multipleImage, _multipleDynamicKey); + } catch (error) { + debugPrint('[LuckGiftBurstSVGA] multiple sync failed: $error'); + } + } + } + + Future _buildAvatarLayerImage(String avatarUrl) async { + final avatarImage = await _loadUiImage( + avatarUrl, + logicalWidth: 100, + logicalHeight: 100, + ); + if (avatarImage == null) { + return null; + } + + const double size = 100; + const double pixelRatio = 3; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(pixelRatio, pixelRatio); + final rect = Rect.fromLTWH(0, 0, size, size); + final center = rect.center; + + canvas.drawCircle( + center, + size / 2, + Paint() + ..color = const Color(0xFFFFD680) + ..isAntiAlias = true, + ); + canvas.save(); + canvas.clipPath(Path()..addOval(rect.deflate(5))); + paintImage( + canvas: canvas, + rect: rect.deflate(5), + image: avatarImage, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ); + canvas.restore(); + + final picture = recorder.endRecording(); + return picture.toImage( + (size * pixelRatio).round(), + (size * pixelRatio).round(), + ); + } + + Future _buildTextLayerImage({ + required String text, + required double logicalWidth, + required double logicalHeight, + required double fontSize, + required Color color, + required Color shadowColor, + }) async { + const double pixelRatio = 3; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(pixelRatio, pixelRatio); + canvas.clipRect(Rect.fromLTWH(0, 0, logicalWidth, logicalHeight)); + + final textPainter = TextPainter( + textDirection: TextDirection.ltr, + maxLines: 1, + textAlign: TextAlign.center, + text: TextSpan( + text: text, + style: TextStyle( + fontSize: fontSize, + color: color, + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + height: 1, + decoration: TextDecoration.none, + shadows: [ + Shadow( + color: shadowColor, + blurRadius: 10, + offset: const Offset(0, 3), ), ], ), - child: ClipOval( - child: netImage( - url: avatarUrl, - width: innerSize, - height: innerSize, - fit: BoxFit.cover, - shape: BoxShape.circle, - ), - ), ), + )..layout(); + + final maxTextWidth = logicalWidth - 8; + final maxTextHeight = logicalHeight - 6; + final scale = math.min( + 1.0, + math.min( + maxTextWidth / math.max(textPainter.width, 1), + maxTextHeight / math.max(textPainter.height, 1), + ), + ); + canvas.save(); + canvas.translate(logicalWidth / 2, logicalHeight / 2); + canvas.scale(scale, scale); + textPainter.paint( + canvas, + Offset(-textPainter.width / 2, -textPainter.height / 2), + ); + canvas.restore(); + + final picture = recorder.endRecording(); + return picture.toImage( + (logicalWidth * pixelRatio).round(), + (logicalHeight * pixelRatio).round(), + ); + } + + Future _loadUiImage( + String resource, { + double? logicalWidth, + double? logicalHeight, + }) async { + final target = resource.trim(); + if (target.isEmpty) { + return null; + } + final provider = buildCachedImageProvider( + target, + logicalWidth: logicalWidth, + logicalHeight: logicalHeight, + ); + final stream = provider.resolve(ImageConfiguration.empty); + final completer = Completer(); + late final ImageStreamListener listener; + listener = ImageStreamListener( + (imageInfo, synchronousCall) { + if (!completer.isCompleted) { + completer.complete(imageInfo.image); + } + stream.removeListener(listener); + }, + onError: (Object error, StackTrace? stackTrace) { + if (!completer.isCompleted) { + completer.complete(null); + } + stream.removeListener(listener); + }, + ); + stream.addListener(listener); + return completer.future.timeout( + const Duration(seconds: 2), + onTimeout: () { + stream.removeListener(listener); + return null; + }, ); } @@ -98,32 +253,8 @@ class _LuckGiftNomorAnimWidgetState extends State { builder: (context, provider, child) { final rewardData = provider.currentPlayingLuckGift?.data; if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) { - _currentBurstEventId = null; - _isRewardAmountVisible = false; return const SizedBox.shrink(); } - final screenWidth = ScreenUtil().screenWidth; - final groupShiftX = screenWidth * _groupShiftXRatio; - final groupShiftY = - screenWidth * _groupShiftYRatio + _groupShiftYExtraUnits.w; - final avatarBaseDiameter = screenWidth * _avatarDiameterRatio; - final avatarDiameter = avatarBaseDiameter * _avatarScale; - final avatarInset = (avatarBaseDiameter - avatarDiameter) / 2; - final avatarLeft = - screenWidth * _avatarLeftRatio + - groupShiftX + - avatarInset + - _avatarExtraShiftXUnits.w; - final avatarTop = - screenWidth * _avatarTopRatio + - groupShiftY + - avatarInset + - _avatarExtraShiftYUnits.w; - final awardTop = screenWidth * _awardTopRatio + groupShiftY; - final awardXOffset = screenWidth * _awardXOffsetRatio + groupShiftX; - final multipleTop = screenWidth * _multipleTopRatio + groupShiftY; - final multipleXOffset = - screenWidth * _multipleXOffsetRatio + groupShiftX; final rewardAnimationKey = ValueKey( '${rewardData.sendUserId ?? ""}' '|${rewardData.acceptUserId ?? ""}' @@ -131,132 +262,23 @@ class _LuckGiftNomorAnimWidgetState extends State { '|${rewardData.multiple ?? 0}' '|${rewardData.normalizedMultipleType}', ); - if (_currentBurstEventId != rewardAnimationKey.value) { - _currentBurstEventId = rewardAnimationKey.value; - _isRewardAmountVisible = false; - } return SizedBox( height: 380.w, - child: Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: EdgeInsets.only(top: 10.w), - child: SCSvgaAssetWidget( - key: ValueKey('burst|${rewardAnimationKey.value}'), - assetPath: LuckGiftNomorAnimWidget._rewardBurstAssetPath, - width: ScreenUtil().screenWidth, - height: 380.w, - fit: BoxFit.fitWidth, - allowDrawingOverflow: true, - clearsAfterStop: true, - onPlaybackStarted: () { - if (!mounted) { - return; - } - if (!_isRewardAmountVisible) { - setState(() { - _isRewardAmountVisible = true; - }); - } - }, - onPlaybackCompleted: () { - if (!mounted) { - return; - } - if (_isRewardAmountVisible) { - setState(() { - _isRewardAmountVisible = false; - }); - } - }, - ), - ), - if (_isRewardAmountVisible && - (rewardData.userAvatar ?? '').trim().isNotEmpty) - Positioned( - left: avatarLeft, - top: avatarTop, - child: _buildSenderAvatar( - avatarUrl: (rewardData.userAvatar ?? '').trim(), - outerSize: avatarDiameter, - ), - ), - if (_isRewardAmountVisible) - Positioned( - top: awardTop, - left: 0, - right: 0, - child: Transform.translate( - offset: Offset(awardXOffset, 0), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: screenWidth * 0.34, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _formatAwardAmount(rewardData.awardAmount ?? 0), - maxLines: 1, - style: TextStyle( - fontSize: 28.sp, - color: const Color(0xFFFFF3B6), - fontWeight: FontWeight.w900, - fontStyle: FontStyle.italic, - height: 1, - shadows: const [ - Shadow( - color: Color(0xCC7A3E00), - blurRadius: 12, - offset: Offset(0, 3), - ), - ], - ), - ), - ), - ), - ), - ), - ), - if (_isRewardAmountVisible && (rewardData.multiple ?? 0) > 0) - Positioned( - top: multipleTop, - left: 0, - right: 0, - child: Transform.translate( - offset: Offset(multipleXOffset, 0), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: screenWidth * 0.18, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'x${_formatMultiple(rewardData.multiple ?? 0)}', - maxLines: 1, - style: TextStyle( - fontSize: 17.sp, - color: const Color(0xFFF4F6FF), - fontWeight: FontWeight.w800, - fontStyle: FontStyle.italic, - height: 1, - shadows: const [ - Shadow( - color: Color(0x990B2D72), - blurRadius: 10, - offset: Offset(0, 2), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], + child: Padding( + padding: EdgeInsets.only(top: 10.w), + child: SCSvgaAssetWidget( + key: ValueKey('burst|${rewardAnimationKey.value}'), + assetPath: LuckGiftNomorAnimWidget._rewardBurstAssetPath, + width: ScreenUtil().screenWidth, + height: 380.w, + fit: BoxFit.fitWidth, + allowDrawingOverflow: true, + clearsAfterStop: true, + dynamicIdentity: rewardAnimationKey.value, + movieConfigurer: + (movieEntity) => + _configureRewardBurstSvga(movieEntity, rewardData), + ), ), ); }, diff --git a/lib/ui_kit/widgets/room/empty_mai_select.dart b/lib/ui_kit/widgets/room/empty_mai_select.dart index dd77216..662640a 100644 --- a/lib/ui_kit/widgets/room/empty_mai_select.dart +++ b/lib/ui_kit/widgets/room/empty_mai_select.dart @@ -70,7 +70,7 @@ class EmptyMaiSelect extends StatelessWidget { } } } else { - MicRes? userWheat = provider.roomWheatMap[index]; + MicRes? userWheat = provider.micAtIndexForDisplay(index); if (userWheat?.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { if (provider.isFz()) { @@ -248,7 +248,7 @@ class EmptyMaiSelect extends StatelessWidget { () { Navigator.of(context).pop(); showBottomInCenterDialog( - context!, + context, RoomUserInfoCard(userId: clickUser?.id), ); }, diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart index 008d831..2135dfa 100644 --- a/lib/ui_kit/widgets/room/room_bottom_widget.dart +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -37,6 +37,8 @@ class _RoomBottomWidgetState extends State { static const double _floatingButtonBottomOffset = 54; static const double _giftActionWidth = 52; static const double _circleActionWidth = 46; + static const double _emojiActionWidth = 38; + static const double _chatEmojiGap = 8; static const double _compactGap = 14; static const double _horizontalPadding = 16; @@ -59,7 +61,10 @@ class _RoomBottomWidgetState extends State { builder: (context, bottomSnapshot, child) { return LayoutBuilder( builder: (context, constraints) { - final inputWidth = constraints.maxWidth / 3; + final inputWidth = _resolveInputWidth( + maxWidth: constraints.maxWidth, + showMic: bottomSnapshot.showMic, + ); final giftCenterX = _resolveGiftCenterX( maxWidth: constraints.maxWidth, inputWidth: inputWidth, @@ -107,6 +112,17 @@ class _RoomBottomWidgetState extends State { ); } + Widget _buildChatControls(double inputWidth) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildChatEntry(inputWidth), + SizedBox(width: _chatEmojiGap.w), + _buildEmojiAction(), + ], + ); + } + Widget _buildBottomBar({ required double inputWidth, required bool showMic, @@ -127,7 +143,7 @@ class _RoomBottomWidgetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildChatEntry(inputWidth), + _buildChatControls(inputWidth), giftAction, _buildMicAction(isMic: isMic), menuAction, @@ -137,7 +153,7 @@ class _RoomBottomWidgetState extends State { : Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildChatEntry(inputWidth), + _buildChatControls(inputWidth), const Spacer(), giftAction, SizedBox(width: _compactGap.w), @@ -157,6 +173,44 @@ class _RoomBottomWidgetState extends State { ); } + Widget _buildEmojiAction() { + return SizedBox( + width: _emojiActionWidth.w, + height: 48.w, + child: SCDebounceWidget( + onTap: () { + if (SCRoomUtils.touristCanMsg(context)) { + Navigator.push( + context, + PopRoute(child: const RoomMsgInput(initialShowEmoji: true)), + ); + } + }, + child: Center( + child: Image.asset( + "sc_images/room/sc_icon_room_bottom_emoji.png", + width: 34.w, + height: 34.w, + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ), + ); + } + + double _resolveInputWidth({required double maxWidth, required bool showMic}) { + final contentWidth = maxWidth - (_horizontalPadding.w * 2); + final actionWidth = + _emojiActionWidth.w + + _chatEmojiGap.w + + _giftActionWidth.w + + _circleActionWidth.w * (showMic ? 3 : 2) + + (showMic ? 16.w : _compactGap.w * 2); + final availableWidth = contentWidth - actionWidth; + return availableWidth.clamp(82.w, 112.w).toDouble(); + } + double _resolveGiftCenterX({ required double maxWidth, required double inputWidth, @@ -165,17 +219,28 @@ class _RoomBottomWidgetState extends State { final contentWidth = maxWidth - (_horizontalPadding.w * 2); if (showMic) { final occupiedWidth = - inputWidth + _giftActionWidth.w + _circleActionWidth.w * 3; + inputWidth + + _chatEmojiGap.w + + _emojiActionWidth.w + + _giftActionWidth.w + + _circleActionWidth.w * 3; final gapCount = 4; final gap = ((contentWidth - occupiedWidth) / gapCount).clamp( 0.0, double.infinity, ); - return _horizontalPadding.w + inputWidth + gap + (_giftActionWidth.w / 2); + return _horizontalPadding.w + + inputWidth + + _chatEmojiGap.w + + _emojiActionWidth.w + + gap + + (_giftActionWidth.w / 2); } final fixedWidth = inputWidth + + _chatEmojiGap.w + + _emojiActionWidth.w + _giftActionWidth.w + _circleActionWidth.w * 2 + _compactGap.w * 2; diff --git a/lib/ui_kit/widgets/room/room_emoji_asset_image.dart b/lib/ui_kit/widgets/room/room_emoji_asset_image.dart new file mode 100644 index 0000000..885cdaf --- /dev/null +++ b/lib/ui_kit/widgets/room/room_emoji_asset_image.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +class RoomEmojiAssetImage extends StatefulWidget { + const RoomEmojiAssetImage({ + super.key, + required this.asset, + required this.width, + required this.height, + this.fit = BoxFit.contain, + }); + + final String asset; + final double width; + final double height; + final BoxFit fit; + + @override + State createState() => _RoomEmojiAssetImageState(); +} + +class _RoomEmojiAssetImageState extends State + with AutomaticKeepAliveClientMixin { + late AssetImage _imageProvider; + + @override + void initState() { + super.initState(); + _imageProvider = AssetImage(widget.asset); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _precache(); + } + + @override + void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.asset != widget.asset) { + _imageProvider = AssetImage(widget.asset); + _precache(); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return RepaintBoundary( + child: Image( + image: _imageProvider, + width: widget.width, + height: widget.height, + fit: widget.fit, + gaplessPlayback: true, + filterQuality: FilterQuality.low, + ), + ); + } + + void _precache() { + precacheImage(_imageProvider, context); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/ui_kit/widgets/room/room_msg_input.dart b/lib/ui_kit/widgets/room/room_msg_input.dart index b5bf07f..516ad5b 100644 --- a/lib/ui_kit/widgets/room/room_msg_input.dart +++ b/lib/ui_kit/widgets/room/room_msg_input.dart @@ -1,10 +1,8 @@ -import 'package:extended_text/extended_text.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/config/business_logic_strategy.dart'; -import 'package:yumi/app/constants/sc_emoji_datas.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_screen.dart'; @@ -14,21 +12,51 @@ import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/tools/sc_keybord_util.dart'; +import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; ///聊天输入框 class RoomMsgInput extends StatefulWidget { final String? atTextContent; + final bool initialShowEmoji; - const RoomMsgInput({super.key, this.atTextContent}); + const RoomMsgInput({ + super.key, + this.atTextContent, + this.initialShowEmoji = false, + }); @override State createState() => _RoomMsgInputState(); } class _RoomMsgInputState extends State { + static const List _roomEmojiAssets = [ + "sc_images/room/emoji/fluent_emoji_01.webp", + "sc_images/room/emoji/fluent_emoji_02.webp", + "sc_images/room/emoji/fluent_emoji_03.webp", + "sc_images/room/emoji/fluent_emoji_04.webp", + "sc_images/room/emoji/fluent_emoji_05.webp", + "sc_images/room/emoji/fluent_emoji_06.webp", + "sc_images/room/emoji/fluent_emoji_07.webp", + "sc_images/room/emoji/fluent_emoji_08.webp", + "sc_images/room/emoji/fluent_emoji_09.webp", + "sc_images/room/emoji/fluent_emoji_10.webp", + "sc_images/room/emoji/monkey_1.gif", + "sc_images/room/emoji/monkey_2.gif", + "sc_images/room/emoji/monkey_3.gif", + "sc_images/room/emoji/monkey_4.gif", + "sc_images/room/emoji/monkey_5.gif", + "sc_images/room/emoji/monkey_6.gif", + "sc_images/room/emoji/monkey_7.gif", + "sc_images/room/emoji/monkey_8.gif", + "sc_images/room/emoji/monkey_9.gif", + "sc_images/room/emoji/monkey_10.gif", + ]; + bool showSend = false; bool showEmoji = false; + bool _emojiAssetsPrecached = false; final FocusNode msgNode = FocusNode(); final TextEditingController controller = TextEditingController(); @@ -39,6 +67,7 @@ class _RoomMsgInputState extends State { @override void initState() { super.initState(); + showEmoji = widget.initialShowEmoji; if (widget.atTextContent != null) { controller.value = TextEditingValue(text: widget.atTextContent ?? ""); showSend = controller.text.trim().isNotEmpty; @@ -52,6 +81,14 @@ class _RoomMsgInputState extends State { super.dispose(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (showEmoji) { + _precacheRoomEmojis(); + } + } + @override Widget build(BuildContext context) { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; @@ -101,8 +138,8 @@ class _RoomMsgInputState extends State { child: Image.asset( showEmoji ? _strategy.getSCMessageChatPageChatKeyboardIcon() - : _strategy.getSCMessageChatPageEmojiIcon(), - color: Colors.white, + : "sc_images/room/sc_icon_room_bottom_emoji.png", + color: showEmoji ? Colors.white : null, fit: BoxFit.contain, ), ), @@ -135,7 +172,7 @@ class _RoomMsgInputState extends State { widget.atTextContent != null ? TextDirection.ltr : null, specialTextSpanBuilder: AtTextSpanBuilder(), focusNode: msgNode, - autofocus: true, + autofocus: !widget.initialShowEmoji, textInputAction: TextInputAction.send, onTap: () { if (showEmoji) { @@ -210,16 +247,21 @@ class _RoomMsgInputState extends State { crossAxisSpacing: 8.w, mainAxisSpacing: 8.w, ), - itemCount: SCEmojiDatas.smileys.length, + itemCount: _roomEmojiAssets.length, itemBuilder: (context, index) { - final emoji = SCEmojiDatas.smileys[index]; + final emojiAsset = _roomEmojiAssets[index]; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - _insertEmoji(emoji); + _sendRoomEmoji(emojiAsset); }, child: Center( - child: ExtendedText(emoji, style: TextStyle(fontSize: sp(15))), + child: RoomEmojiAssetImage( + key: ValueKey(emojiAsset), + asset: emojiAsset, + width: 30.w, + height: 30.w, + ), ), ); }, @@ -237,30 +279,47 @@ class _RoomMsgInputState extends State { } SCKeybordUtil.hide(context); + _precacheRoomEmojis(); setState(() { showEmoji = true; }); } - void _insertEmoji(String emoji) { - final value = controller.value; - final selection = value.selection; - final fallbackOffset = value.text.length; - final start = selection.isValid ? selection.start : fallbackOffset; - final end = selection.isValid ? selection.end : fallbackOffset; - final safeStart = start < 0 ? fallbackOffset : start; - final safeEnd = end < 0 ? fallbackOffset : end; - final nextText = value.text.replaceRange(safeStart, safeEnd, emoji); + void _precacheRoomEmojis() { + if (_emojiAssetsPrecached) { + return; + } + _emojiAssetsPrecached = true; + for (final asset in _roomEmojiAssets) { + precacheImage(AssetImage(asset), context); + } + } - controller.value = value.copyWith( - text: nextText, - selection: TextSelection.collapsed(offset: safeStart + emoji.length), - composing: TextRange.empty, + void _sendRoomEmoji(String emojiAsset) { + final rtcProvider = Provider.of(context, listen: false); + final currenRoom = rtcProvider.currenRoom; + final currentUser = AccountStorage().getCurrentUser()?.userProfile; + if (currenRoom == null || currentUser == null) { + return; + } + + final seatIndex = rtcProvider.userOnMaiInIndex(currentUser.id ?? ""); + final msg = Msg( + groupId: currenRoom.roomProfile?.roomProfile?.roomAccount ?? "", + role: rtcProvider.currenRoom?.entrants?.roles ?? "", + msg: emojiAsset, + type: SCRoomMsgType.emoticons, + user: currentUser, + number: seatIndex, ); - setState(() { - showSend = controller.text.trim().isNotEmpty; - }); + if (seatIndex > -1) { + rtcProvider.starPlayEmoji(msg); + } + Provider.of( + context, + listen: false, + ).dispatchMessage(msg, addLocal: seatIndex < 0); } void _sendMessage() { diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index f4c1b80..0154aa3 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -1,1405 +1,1483 @@ -import 'dart:convert'; -import 'package:extended_image/extended_image.dart' - show ExtendedNetworkImageProvider; -import 'package:extended_text/extended_text.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; -import 'package:yumi/app/constants/sc_room_msg_type.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; -import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; -import 'package:yumi/modules/index/main_route.dart'; - -import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart'; -import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; -import '../../components/sc_compontent.dart'; -import '../../components/sc_tts.dart'; -import '../../components/text/sc_text.dart'; - -///消息item -class MsgItem extends StatefulWidget { - final Function(SocialChatUserProfile? user) onClick; - Msg msg; - - MsgItem({Key? key, required this.msg, required this.onClick}) - : super(key: key); - - @override - _MsgItemState createState() => _MsgItemState(); -} - -class _MsgItemState extends State { - @override - Widget build(BuildContext context) { - //gift - // if (msg.type == Roomtype.gift.code) { - // return _buildGiftMsg(context); - // } - - ///系统提示 - if (widget.msg.type == SCRoomMsgType.systemTips) { - return _systemTipsRoomItem(context); - } - - ///进入房间 - if (widget.msg.type == SCRoomMsgType.joinRoom) { - return _enterRoomItem(context); - } - - ///礼物 - if (widget.msg.type == SCRoomMsgType.gift || - widget.msg.type == SCRoomMsgType.luckGiftAnimOther) { - return _buildGiftMsg(context); - } - - ///火箭用户中奖 - if (widget.msg.type == SCRoomMsgType.rocketRewardUser) { - return _buildRocketRewardMsg(context); - } - - ///幸运礼物 - if (widget.msg.type == SCRoomMsgType.gameLuckyGift) { - return _buildGameLuckyGiftMsg(context); - } - - ///幸运礼物 - if (widget.msg.type == SCRoomMsgType.gameLuckyGift_5) { - return _buildGameLuckyGiftMsg_5(context); - } - - ///房间身份变动 - if (widget.msg.type == SCRoomMsgType.roomRoleChange) { - return _buildRoleChangeMsg(context); - } - - ///踢出房间 - if (widget.msg.type == SCRoomMsgType.qcfj) { - widget.msg.msg = SCAppLocalizations.of(context)!.kickRoomTips; - } - - ///图片消息 - if (widget.msg.type == SCRoomMsgType.image) { - return _buildImageMsg(context); - } - - ///掷骰子 - if (widget.msg.type == SCRoomMsgType.roomDice) { - return _buildDiceMsg(context); - } - - ///石头剪刀布 - if (widget.msg.type == SCRoomMsgType.roomRPS) { - return _buildRPSMsg(context); - } - - ///幸运数字 - if (widget.msg.type == SCRoomMsgType.roomLuckNumber) { - return _buildLuckNumberMsg(context); - } - if (widget.msg.user != null) { - PropsResources? chatBox = widget.msg.user?.getChatBox(); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(5.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: GestureDetector( - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: ExtendedText( - widget.msg?.msg ?? '', - style: TextStyle( - fontSize: sp(13), - color: Color(_msgColor(widget.msg.type ?? "")), - fontWeight: FontWeight.w500, - ), - softWrap: true, // 自动换行 - specialTextSpanBuilder: AtTextSpanBuilder( - onTapCall: (userId) {}, - ), - ), - ), - onLongPress: () { - if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) { - Clipboard.setData( - ClipboardData(text: widget.msg?.msg ?? ''), - ); - SCTts.show("Copied"); - } - }, - ), - ), - ], - ), - ], - ), - ), - ); - } else { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(8.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.all(Radius.circular(15.w)), - ), - child: Text( - widget.msg?.msg ?? '', - style: TextStyle( - fontSize: sp(13), - color: Color(_msgColor(widget.msg.type ?? "")), - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.left, - ), - ), - ); - } - } - - ///系统提示 - _systemTipsRoomItem(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(8.w), - vertical: width(8), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 8.w), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.all(Radius.circular(15.w)), - ), - child: Text( - widget.msg?.msg ?? '', - style: TextStyle( - fontSize: sp(13), - height: 1.5.w, - color: Color(_msgColor(widget.msg.type ?? "")), - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.left, - ), - ), - ); - } - - ///火箭用户中奖 - _buildRocketRewardMsg(BuildContext context) { - List spans = []; - - spans.add( - TextSpan( - text: "${widget.msg.user?.userNickname} ", - style: TextStyle( - fontSize: sp(12), - color: SocialChatTheme.primaryColor, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - TextSpan( - text: SCAppLocalizations.of(context)!.inRocket, - style: TextStyle( - fontSize: sp(12), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Image.asset( - "sc_images/room/sc_icon_room_rocket_lv${widget.msg.number}.png", - width: 18.w, - ), - ), - ); - spans.add( - TextSpan( - text: SCAppLocalizations.of(context)!.obtain, - style: TextStyle( - fontSize: sp(12), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: - widget.msg.role == SCActivityRewardATPropsType.GOLD.name - ? Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w) - : netImage(url: widget.msg.msg ?? "", width: 18.w), - ), - ); - Widget text = Text.rich( - TextSpan(children: spans), - textAlign: TextAlign.left, - strutStyle: StrutStyle( - height: 1.8, // 行高倍数 - fontWeight: FontWeight.w500, - forceStrutHeight: true, // 强制应用行高 - ), - ); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), - margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.all(Radius.circular(15.w)), - ), - child: text, - ), - ); - } - - ///幸运礼物 - _buildGameLuckyGiftMsg(BuildContext context) { - List spans = []; - - spans.add( - TextSpan( - text: "${widget.msg.user?.userNickname} ", - style: TextStyle( - fontSize: sp(12), - color: SocialChatTheme.primaryColor, - fontWeight: FontWeight.w500, - ), - recognizer: - TapGestureRecognizer() - ..onTap = () { - widget.onClick(widget.msg.user); - }, - ), - ); - spans.add( - TextSpan( - text: SCAppLocalizations.of(context)!.sendTo, - style: TextStyle( - fontSize: sp(12), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - TextSpan( - text: " ${widget.msg.toUser?.userNickname} ", - style: TextStyle( - fontSize: sp(12), - color: SocialChatTheme.primaryColor, - fontWeight: FontWeight.w500, - ), - recognizer: - TapGestureRecognizer() - ..onTap = () { - widget.onClick(widget.msg.toUser); - }, - ), - ); - SCGlobalConfig.lang == "ar" - ? spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), - ), - ) - : spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), - ), - ); - spans.add( - TextSpan( - text: " ${SCAppLocalizations.of(context)!.obtain}", - style: TextStyle( - fontSize: sp(12), - color: SocialChatTheme.primaryColor, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - TextSpan( - text: " ${_formatAwardAmount(widget.msg.awardAmount)}", - style: TextStyle( - fontSize: sp(12), - color: SocialChatTheme.primaryColor, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - SCGlobalConfig.lang == "ar" - ? spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), - ), - ) - : spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), - ), - ); - Widget text = Text.rich( - TextSpan(children: spans), - textAlign: TextAlign.left, - strutStyle: StrutStyle( - height: 1.3, // 行高倍数 - fontWeight: FontWeight.w500, - forceStrutHeight: true, // 强制应用行高 - ), - ); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), - margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.all(Radius.circular(15.w)), - ), - child: text, - ), - ); - } - - ///幸运礼物中奖5倍以及以上 - _buildGameLuckyGiftMsg_5(BuildContext context) { - List spans = []; - spans.add( - TextSpan( - text: "${widget.msg.user?.userNickname} ", - style: TextStyle( - fontSize: sp(12), - color: Color(0xffFEF129), - fontWeight: FontWeight.w500, - ), - recognizer: - TapGestureRecognizer() - ..onTap = () { - widget.onClick(widget.msg.user); - }, - ), - ); - spans.add( - TextSpan( - text: SCAppLocalizations.of(context)!.get, - style: TextStyle( - fontSize: sp(12), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - TextSpan( - text: " ${_formatAwardAmount(widget.msg.awardAmount)} ", - style: TextStyle( - fontSize: sp(12), - color: Color(0xffFEF129), - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Image.asset( - "sc_images/general/sc_icon_jb.png", - width: 18.w, - height: 18.w, - ), - ), - ); - spans.add( - TextSpan( - text: " from ", - style: TextStyle( - fontSize: sp(12), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: netImage( - url: widget.msg.gift?.giftPhoto ?? "", - width: 18.w, - height: 18.w, - ), - ), - ); - Widget text = Text.rich( - TextSpan(children: spans), - textAlign: TextAlign.left, - strutStyle: StrutStyle( - height: 1.2, // 行高倍数 - fontWeight: FontWeight.w500, - forceStrutHeight: true, // 强制应用行高 - ), - ); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - constraints: BoxConstraints(minHeight: 68.w), - padding: EdgeInsets.symmetric(vertical: 5.w), - margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_luck_gift_msg_n_bg.png"), - fit: BoxFit.fill, - ), - ), - child: Row( - children: [ - SizedBox(width: 10.w), - Expanded(child: text), - Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/room/sc_icon_luck_gift_msg_n_ball.png", - ), - fit: BoxFit.fill, - ), - ), - alignment: AlignmentDirectional.center, - width: 55.w, - height: 60.w, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 10.w), - buildNumForGame(widget.msg.msg ?? "0", size: 18.w), - SizedBox(height: 3.w), - SCGlobalConfig.lang == "ar" - ? Image.asset( - "sc_images/room/sc_icon_times_text_ar.png", - height: 12.w, - ) - : Image.asset( - "sc_images/room/sc_icon_times_text_en.png", - height: 10.w, - ), - ], - ), - ), - SizedBox(width: 15.w), - ], - ), - ), - ); - } - - String _formatAwardAmount(num? awardAmount) { - if (awardAmount == null) { - return "0"; - } - if (awardAmount % 1 == 0) { - return awardAmount.toInt().toString(); - } - return awardAmount.toString(); - } - - Widget _buildGiftMsg(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)), - margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: SCAppLocalizations.of(context)!.sendTo, - style: TextStyle( - fontSize: 13.sp, - color: Colors.white, - ), - ), - TextSpan(text: " "), - TextSpan( - text: widget.msg.toUser?.userNickname ?? "", - style: TextStyle( - fontSize: 13.sp, - color: SocialChatTheme.primaryColor, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - TextSpan(text: " "), - WidgetSpan( - child: netImage( - url: widget.msg.gift?.giftPhoto ?? "", - width: 22.w, - height: 22.w, - ), - ), - TextSpan(text: " "), - TextSpan( - text: "x${widget.msg.number}", - style: TextStyle( - fontSize: 13.sp, - color: Colors.white, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ], - ), - textAlign: TextAlign.left, - ), - ), - ), - ], - ), - ], - ), - ); - } - - ///进入房间 - _enterRoomItem(BuildContext context) { - List spans = []; - if (widget.msg.role != null && widget.msg.role != 0) { - spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: EdgeInsets.only(right: 3.w), - child: msgRoleTag(widget.msg.role ?? "", width: 18.w), - ), - ), - ); - } - spans.add( - TextSpan( - text: "${widget.msg?.user?.userNickname ?? ''} ", - style: TextStyle( - fontSize: sp(13), - color: _roleNameColor(widget.msg.role ?? ""), - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - spans.add( - TextSpan( - text: SCAppLocalizations.of(context)!.joinRoomTips, - style: TextStyle( - fontSize: sp(13), - color: Color(_msgColor(widget.msg.type ?? "")), - fontWeight: FontWeight.w500, - ), - ), - ); - Widget text = Text.rich( - TextSpan(children: spans), - textAlign: TextAlign.left, - ); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(8.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 8.w), - child: text, - ), - ); - } - - Widget _buildMedals(List wearBadge) { - return wearBadge.isNotEmpty - ? Expanded( - child: SingleChildScrollView( - child: SizedBox( - height: 25.w, - child: ListView.separated( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemCount: wearBadge.length ?? 0, - itemBuilder: (context, index) { - return netImage( - width: 25.w, - height: 25.w, - url: wearBadge[index].selectUrl ?? "", - ); - }, - separatorBuilder: (BuildContext context, int index) { - return SizedBox(width: 5.w); - }, - ), - ), - ), - ) - : Container(); - } - - int _msgColor(String type) { - switch (type) { - case SCRoomMsgType.joinRoom: - case SCRoomMsgType.systemTips: - case SCRoomMsgType.welcome: - return 0xFFFFFFFF; - case SCRoomMsgType.gift: - return 0xFFFFFFFF; - case SCRoomMsgType.shangMai: - case SCRoomMsgType.xiaMai: - case SCRoomMsgType.qcfj: - case SCRoomMsgType.killXiaMai: - return 0xFFFFFFFF; - break; - // case type.text: - // return _mapTextColor(); - // break; - default: - return 0xFFFFFFFF; - } - } - - _roleNameColor(String role) { - if (SCRoomRolesType.HOMEOWNER.name == role) { - return Color(0xff2AC0F2); - } else if (SCRoomRolesType.ADMIN.name == role) { - return Color(0xffF9A024); - } else if (SCRoomRolesType.MEMBER.name == role) { - return Color(0xff28ED77); - } else { - return Colors.white; - } - } - - Widget _buildRoleChangeMsg(BuildContext context) { - var text = ""; - if (SCRoomRolesType.ADMIN.name == widget.msg.msg) { - text = SCAppLocalizations.of(context)!.adminByHomeowner; - } else if (SCRoomRolesType.MEMBER.name == widget.msg.msg) { - text = SCAppLocalizations.of(context)!.memberByHomeowner; - } else { - text = SCAppLocalizations.of(context)!.touristByHomeowner; - } - List spans = []; - if (widget.msg.toUser != null) { - spans.add( - TextSpan( - text: "${widget.msg.toUser?.userNickname ?? ''} ", - style: TextStyle( - fontSize: sp(13), - color: Color(0xff2AC0F2), - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ); - } - spans.add( - TextSpan( - text: text, - style: TextStyle( - fontSize: sp(13), - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), - ); - Widget textView = Text.rich( - TextSpan(children: spans), - textAlign: TextAlign.left, - ); - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(8.w), - vertical: width(8), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 8.w), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.all(Radius.circular(15.w)), - ), - child: textView, - ), - ); - } - - Widget _buildImageMsg(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(5.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: GestureDetector( - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: netImage(url: widget.msg.msg ?? ""), - ), - onTap: () { - String encodedUrls = Uri.encodeComponent( - jsonEncode([widget.msg.msg]), - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", - ); - }, - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildDiceMsg(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(5.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: GestureDetector( - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: - (widget.msg.hasPlayedAnimation ?? false) - ? Image.asset( - "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", - height: 35.w, - ) - : FutureBuilder( - future: Future.delayed( - Duration(milliseconds: 2000), - ), - // 传入Future - builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { - // 根据snapshot的状态来构建UI - if (snapshot.connectionState == - ConnectionState.done) { - widget.msg.hasPlayedAnimation = true; - return Image.asset( - "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", - height: 35.w, - ); - } else { - return Image.asset( - "sc_images/room/sc_icon_dice_animl.webp", - height: 35.w, - ); - } - }, - ), - ), - onTap: () {}, - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildRPSMsg(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(5.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: GestureDetector( - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: - (widget.msg.hasPlayedAnimation ?? false) - ? Image.asset( - "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", - height: 35.w, - ) - : FutureBuilder( - future: Future.delayed( - Duration(milliseconds: 2000), - ), - // 传入Future - builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { - // 根据snapshot的状态来构建UI - if (snapshot.connectionState == - ConnectionState.done) { - widget.msg.hasPlayedAnimation = true; - return Image.asset( - "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", - height: 35.w, - ); - } else { - return Image.asset( - "sc_images/room/sc_icon_rps_animal.webp", - height: 35.w, - ); - } - }, - ), - ), - onTap: () {}, - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildLuckNumberMsg(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topStart, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: width(5.w), - vertical: width(4), - ), - margin: EdgeInsets.symmetric( - horizontal: width(10), - ).copyWith(bottom: 6.w), - child: Column( - children: [ - _buildUserInfoLine(context), - SizedBox(height: 6.w), - Row( - children: [ - Flexible( - // 使用Flexible替代Expanded - child: GestureDetector( - child: Container( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 - ), - decoration: BoxDecoration( - color: Colors.black38, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.w), - bottomLeft: Radius.circular(15.w), - bottomRight: Radius.circular(15.w), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 4.w, - ), - margin: EdgeInsetsDirectional.only( - start: 54.w, - ).copyWith(bottom: 8.w), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SCGlobalConfig.lang == "ar" - ? Image.asset( - "sc_images/room/sc_icon_luck_num_text_ar.png", - height: 20.w, - ) - : Image.asset( - "sc_images/room/sc_icon_luck_num_text_en.png", - height: 20.w, - ), - buildNumForRoomLuckNum( - widget.msg.msg ?? "", - size: 20.w, - ), - ], - ), - ), - onTap: () {}, - ), - ), - ], - ), - ], - ), - ), - ); - } - - _buildUserInfoLine(BuildContext context) { - final userId = widget.msg.user?.id; - if ((widget.msg.needUpDataUserInfo) ?? false) { - SCRoomUtils.roomUsersMap.remove(userId ?? ""); - } - if (userId == null || userId.isEmpty) { - return _buildPendingUserInfoLine(context); - } - return SCRoomUtils.roomUsersMap[userId] == null - ? FutureBuilder( - future: SCAccountRepository().loadUserInfo(userId), - builder: (ct, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasData) { - SocialChatUserProfile user = snapshot.data!; - SCRoomUtils.roomUsersMap[user.id ?? ""] = user; - return _buildNewUserInfoLine(context, user); - } - } - return _buildPendingUserInfoLine(context); - }, - ) - : _buildNewUserInfoLine(context, SCRoomUtils.roomUsersMap[userId]!); - } - - Widget _buildPendingUserInfoLine(BuildContext context) { - return Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - widget.onClick(widget.msg.user); - }, - child: Stack( - alignment: AlignmentDirectional.bottomEnd, - children: [ - head( - url: widget.msg.user?.userAvatar ?? "", - width: 48.w, - // headdress: msg.user?.getHeaddress()?.sourceUrl, - ), - Positioned( - child: msgRoleTag(widget.msg.role ?? "", width: 14.w), - bottom: 2.w, - right: 2.w, - ), - ], - ), - ), - SizedBox(width: 3.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - getWealthLevel( - widget.msg.user?.wealthLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - getUserLevel( - widget.msg.user?.charmLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - SizedBox(width: 3.w), - socialchatNickNameText( - maxWidth: 120.w, - fontWeight: FontWeight.w500, - widget.msg.user?.userNickname ?? "", - fontSize: 13.sp, - type: widget.msg.user?.getVIP()?.name ?? "", - needScroll: - (widget.msg.user?.userNickname?.characters.length ?? - 0) > - 10, - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - widget.msg.user?.getVIP() != null - ? netImage( - url: widget.msg.user?.getVIP()?.cover ?? "", - width: 25.w, - height: 25.w, - ) - : Container(), - _buildMedals( - (widget.msg.user?.wearBadge?.where((item) { - return item.use ?? false; - }).toList() ?? - []), - ), - ], - ), - ], - ), - ), - ], - ); - } - - _buildNewUserInfoLine(BuildContext context, SocialChatUserProfile user) { - return Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - widget.onClick(user); - }, - child: Stack( - alignment: AlignmentDirectional.bottomEnd, - children: [ - head( - url: user.userAvatar ?? "", - width: 48.w, - // headdress: msg.user?.getHeaddress()?.sourceUrl, - ), - Positioned( - child: msgRoleTag(widget.msg.role ?? "", width: 14.w), - bottom: 2.w, - right: 2.w, - ), - ], - ), - ), - SizedBox(width: 3.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - getWealthLevel( - user.wealthLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - getUserLevel( - user.charmLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - SizedBox(width: 3.w), - socialchatNickNameText( - maxWidth: 120.w, - fontWeight: FontWeight.w500, - user.userNickname ?? "", - fontSize: 13.sp, - type: user.getVIP()?.name ?? "", - needScroll: - (user.userNickname?.characters.length ?? 0) > 10, - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - user.getVIP() != null - ? netImage( - url: user.getVIP()?.cover ?? "", - width: 25.w, - height: 25.w, - ) - : Container(), - _buildMedals( - user.wearBadge?.where((item) { - return item.use ?? false; - }).toList() ?? - [], - ), - ], - ), - ], - ), - ), - ], - ); - } -} - -///消息 -class Msg { - String? groupId; - String? msg; - MicRes? userWheat; - String? type; - SocialChatUserProfile? user; - String? role = ""; - SocialChatGiftRes? gift; - - //礼物个数 - num? number; - - // 自定义动画需要播放的次数 - int? customAnimationCount; - - num? awardAmount; - - // 送礼给谁 - SocialChatUserProfile? toUser; - int? time = 0; - - //动画是否已经播放(骰子、石头剪刀布、随机数字) - bool? hasPlayedAnimation = false; - - bool? needUpDataUserInfo = false; - - Msg({ - required this.groupId, - required this.msg, - required this.type, - this.user, - this.toUser, - this.userWheat, - this.role = "", - this.gift, - this.number, - this.customAnimationCount, - this.awardAmount, - this.hasPlayedAnimation, - this.needUpDataUserInfo, - }) { - time = DateTime.now().millisecondsSinceEpoch; - } - - Msg.fromJson(dynamic json) { - groupId = json['groupId']; - msg = json['msg']; - type = json['type']; - role = json['role']; - time = json['time']; - number = json['number']; - customAnimationCount = json['customAnimationCount']; - awardAmount = json['awardAmount']; - hasPlayedAnimation = json['hasPlayedAnimation']; - needUpDataUserInfo = json['needUpDataUserInfo']; - userWheat = - json['userWheat'] != null ? MicRes.fromJson(json['userWheat']) : null; - user = - json['user'] != null - ? SocialChatUserProfile.fromJson(json['user']) - : null; - toUser = - json['toUser'] != null - ? SocialChatUserProfile.fromJson(json['toUser']) - : null; - - gift = - json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null; - } - - Map toJson() { - final map = {}; - map['groupId'] = groupId; - map['msg'] = msg; - map['type'] = type; - map['role'] = role; - map['time'] = time; - map['awardAmount'] = awardAmount; - map['number'] = number; - map['customAnimationCount'] = customAnimationCount; - map['hasPlayedAnimation'] = hasPlayedAnimation; - map['needUpDataUserInfo'] = needUpDataUserInfo; - if (userWheat != null) { - map['userWheat'] = userWheat?.toJson(); - } - if (user != null) { - map['user'] = user?.toJson(); - } - if (toUser != null) { - map['toUser'] = toUser?.toJson(); - } - if (gift != null) { - map['gift'] = gift?.toJson(); - } - return map; - } -} +import 'dart:convert'; +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; + +import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart'; +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../components/sc_compontent.dart'; +import '../../components/sc_tts.dart'; +import '../../components/text/sc_text.dart'; + +///消息item +class MsgItem extends StatefulWidget { + final Function(SocialChatUserProfile? user) onClick; + Msg msg; + + MsgItem({Key? key, required this.msg, required this.onClick}) + : super(key: key); + + @override + _MsgItemState createState() => _MsgItemState(); +} + +class _MsgItemState extends State { + @override + Widget build(BuildContext context) { + //gift + // if (msg.type == Roomtype.gift.code) { + // return _buildGiftMsg(context); + // } + + ///系统提示 + if (widget.msg.type == SCRoomMsgType.systemTips) { + return _systemTipsRoomItem(context); + } + + ///进入房间 + if (widget.msg.type == SCRoomMsgType.joinRoom) { + return _enterRoomItem(context); + } + + ///礼物 + if (widget.msg.type == SCRoomMsgType.gift || + widget.msg.type == SCRoomMsgType.luckGiftAnimOther) { + return _buildGiftMsg(context); + } + + ///火箭用户中奖 + if (widget.msg.type == SCRoomMsgType.rocketRewardUser) { + return _buildRocketRewardMsg(context); + } + + ///幸运礼物 + if (widget.msg.type == SCRoomMsgType.gameLuckyGift) { + return _buildGameLuckyGiftMsg(context); + } + + ///幸运礼物 + if (widget.msg.type == SCRoomMsgType.gameLuckyGift_5) { + return _buildGameLuckyGiftMsg_5(context); + } + + ///房间身份变动 + if (widget.msg.type == SCRoomMsgType.roomRoleChange) { + return _buildRoleChangeMsg(context); + } + + ///踢出房间 + if (widget.msg.type == SCRoomMsgType.qcfj) { + widget.msg.msg = SCAppLocalizations.of(context)!.kickRoomTips; + } + + ///图片消息 + if (widget.msg.type == SCRoomMsgType.image) { + return _buildImageMsg(context); + } + + ///表情包聊天消息 + if (widget.msg.type == SCRoomMsgType.emoticons) { + return _buildEmoticonMsg(context); + } + + ///掷骰子 + if (widget.msg.type == SCRoomMsgType.roomDice) { + return _buildDiceMsg(context); + } + + ///石头剪刀布 + if (widget.msg.type == SCRoomMsgType.roomRPS) { + return _buildRPSMsg(context); + } + + ///幸运数字 + if (widget.msg.type == SCRoomMsgType.roomLuckNumber) { + return _buildLuckNumberMsg(context); + } + if (widget.msg.user != null) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: ExtendedText( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + softWrap: true, // 自动换行 + specialTextSpanBuilder: AtTextSpanBuilder( + onTapCall: (userId) {}, + ), + ), + ), + onLongPress: () { + if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) { + Clipboard.setData( + ClipboardData(text: widget.msg?.msg ?? ''), + ); + SCTts.show("Copied"); + } + }, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: Text( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + ), + ), + ); + } + } + + ///系统提示 + _systemTipsRoomItem(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(8), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: Text( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + height: 1.5.w, + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + ), + ), + ); + } + + ///火箭用户中奖 + _buildRocketRewardMsg(BuildContext context) { + List spans = []; + + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.inRocket, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + "sc_images/room/sc_icon_room_rocket_lv${widget.msg.number}.png", + width: 18.w, + ), + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.obtain, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: + widget.msg.role == SCActivityRewardATPropsType.GOLD.name + ? Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w) + : netImage(url: widget.msg.msg ?? "", width: 18.w), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.8, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: text, + ), + ); + } + + ///幸运礼物 + _buildGameLuckyGiftMsg(BuildContext context) { + List spans = []; + + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.user); + }, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.sendTo, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${widget.msg.toUser?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.toUser); + }, + ), + ); + SCGlobalConfig.lang == "ar" + ? spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), + ), + ) + : spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), + ), + ); + spans.add( + TextSpan( + text: " ${SCAppLocalizations.of(context)!.obtain}", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${_formatAwardAmount(widget.msg.awardAmount)}", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + SCGlobalConfig.lang == "ar" + ? spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), + ), + ) + : spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: text, + ), + ); + } + + ///幸运礼物中奖5倍以及以上 + _buildGameLuckyGiftMsg_5(BuildContext context) { + List spans = []; + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: Color(0xffFEF129), + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.user); + }, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.get, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${_formatAwardAmount(widget.msg.awardAmount)} ", + style: TextStyle( + fontSize: sp(12), + color: Color(0xffFEF129), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 18.w, + height: 18.w, + ), + ), + ); + spans.add( + TextSpan( + text: " from ", + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: netImage( + url: widget.msg.gift?.giftPhoto ?? "", + width: 18.w, + height: 18.w, + ), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.2, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + constraints: BoxConstraints(minHeight: 68.w), + padding: EdgeInsets.symmetric(vertical: 5.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_luck_gift_msg_n_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Expanded(child: text), + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/room/sc_icon_luck_gift_msg_n_ball.png", + ), + fit: BoxFit.fill, + ), + ), + alignment: AlignmentDirectional.center, + width: 55.w, + height: 60.w, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 10.w), + buildNumForGame(widget.msg.msg ?? "0", size: 18.w), + SizedBox(height: 3.w), + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_times_text_ar.png", + height: 12.w, + ) + : Image.asset( + "sc_images/room/sc_icon_times_text_en.png", + height: 10.w, + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + ); + } + + String _formatAwardAmount(num? awardAmount) { + if (awardAmount == null) { + return "0"; + } + if (awardAmount % 1 == 0) { + return awardAmount.toInt().toString(); + } + return awardAmount.toString(); + } + + Widget _buildGiftMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)), + margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: SCAppLocalizations.of(context)!.sendTo, + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), + ), + TextSpan(text: " "), + TextSpan( + text: widget.msg.toUser?.userNickname ?? "", + style: TextStyle( + fontSize: 13.sp, + color: SocialChatTheme.primaryColor, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + TextSpan(text: " "), + WidgetSpan( + child: netImage( + url: widget.msg.gift?.giftPhoto ?? "", + width: 22.w, + height: 22.w, + ), + ), + TextSpan(text: " "), + TextSpan( + text: "x${widget.msg.number}", + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ], + ), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ], + ), + ); + } + + ///进入房间 + _enterRoomItem(BuildContext context) { + List spans = []; + if (widget.msg.role != null && widget.msg.role != 0) { + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.only(right: 3.w), + child: msgRoleTag(widget.msg.role ?? "", width: 18.w), + ), + ), + ); + } + spans.add( + TextSpan( + text: "${widget.msg?.user?.userNickname ?? ''} ", + style: TextStyle( + fontSize: sp(13), + color: _roleNameColor(widget.msg.role ?? ""), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.joinRoomTips, + style: TextStyle( + fontSize: sp(13), + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + child: text, + ), + ); + } + + Widget _buildMedals(List wearBadge) { + return wearBadge.isNotEmpty + ? Expanded( + child: SingleChildScrollView( + child: SizedBox( + height: 25.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: wearBadge.length ?? 0, + itemBuilder: (context, index) { + return netImage( + width: 25.w, + height: 25.w, + url: wearBadge[index].selectUrl ?? "", + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + ) + : Container(); + } + + int _msgColor(String type) { + switch (type) { + case SCRoomMsgType.joinRoom: + case SCRoomMsgType.systemTips: + case SCRoomMsgType.welcome: + return 0xFFFFFFFF; + case SCRoomMsgType.gift: + return 0xFFFFFFFF; + case SCRoomMsgType.shangMai: + case SCRoomMsgType.xiaMai: + case SCRoomMsgType.qcfj: + case SCRoomMsgType.killXiaMai: + return 0xFFFFFFFF; + break; + // case type.text: + // return _mapTextColor(); + // break; + default: + return 0xFFFFFFFF; + } + } + + _roleNameColor(String role) { + if (SCRoomRolesType.HOMEOWNER.name == role) { + return Color(0xff2AC0F2); + } else if (SCRoomRolesType.ADMIN.name == role) { + return Color(0xffF9A024); + } else if (SCRoomRolesType.MEMBER.name == role) { + return Color(0xff28ED77); + } else { + return Colors.white; + } + } + + Widget _buildRoleChangeMsg(BuildContext context) { + var text = ""; + if (SCRoomRolesType.ADMIN.name == widget.msg.msg) { + text = SCAppLocalizations.of(context)!.adminByHomeowner; + } else if (SCRoomRolesType.MEMBER.name == widget.msg.msg) { + text = SCAppLocalizations.of(context)!.memberByHomeowner; + } else { + text = SCAppLocalizations.of(context)!.touristByHomeowner; + } + List spans = []; + if (widget.msg.toUser != null) { + spans.add( + TextSpan( + text: "${widget.msg.toUser?.userNickname ?? ''} ", + style: TextStyle( + fontSize: sp(13), + color: Color(0xff2AC0F2), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + } + spans.add( + TextSpan( + text: text, + style: TextStyle( + fontSize: sp(13), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ); + Widget textView = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(8), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: textView, + ), + ); + } + + Widget _buildImageMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: netImage(url: widget.msg.msg ?? ""), + ), + onTap: () { + String encodedUrls = Uri.encodeComponent( + jsonEncode([widget.msg.msg]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildEmoticonMsg(BuildContext context) { + final emojiPath = widget.msg.msg ?? ""; + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.45, + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 8.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: _buildEmojiImage( + emojiPath, + width: 72.w, + height: 72.w, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildEmojiImage( + String path, { + required double width, + required double height, + }) { + if (path.isEmpty) { + return SizedBox(width: width, height: height); + } + if (_isLocalAsset(path)) { + return RoomEmojiAssetImage( + key: ValueKey(path), + asset: path, + width: width, + height: height, + ); + } + return netImage(url: path, width: width, height: height); + } + + bool _isLocalAsset(String path) { + return path.startsWith("assets/") || path.startsWith("sc_images/"); + } + + Widget _buildDiceMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: + (widget.msg.hasPlayedAnimation ?? false) + ? Image.asset( + "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", + height: 35.w, + ) + : FutureBuilder( + future: Future.delayed( + Duration(milliseconds: 2000), + ), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == + ConnectionState.done) { + widget.msg.hasPlayedAnimation = true; + return Image.asset( + "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", + height: 35.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_dice_animl.webp", + height: 35.w, + ); + } + }, + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildRPSMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: + (widget.msg.hasPlayedAnimation ?? false) + ? Image.asset( + "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", + height: 35.w, + ) + : FutureBuilder( + future: Future.delayed( + Duration(milliseconds: 2000), + ), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == + ConnectionState.done) { + widget.msg.hasPlayedAnimation = true; + return Image.asset( + "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", + height: 35.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_rps_animal.webp", + height: 35.w, + ); + } + }, + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildLuckNumberMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child: Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_luck_num_text_ar.png", + height: 20.w, + ) + : Image.asset( + "sc_images/room/sc_icon_luck_num_text_en.png", + height: 20.w, + ), + buildNumForRoomLuckNum( + widget.msg.msg ?? "", + size: 20.w, + ), + ], + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + _buildUserInfoLine(BuildContext context) { + final userId = widget.msg.user?.id; + if ((widget.msg.needUpDataUserInfo) ?? false) { + SCRoomUtils.roomUsersMap.remove(userId ?? ""); + } + if (userId == null || userId.isEmpty) { + return _buildPendingUserInfoLine(context); + } + return SCRoomUtils.roomUsersMap[userId] == null + ? FutureBuilder( + future: SCAccountRepository().loadUserInfo(userId), + builder: (ct, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasData) { + SocialChatUserProfile user = snapshot.data!; + SCRoomUtils.roomUsersMap[user.id ?? ""] = user; + return _buildNewUserInfoLine(context, user); + } + } + return _buildPendingUserInfoLine(context); + }, + ) + : _buildNewUserInfoLine(context, SCRoomUtils.roomUsersMap[userId]!); + } + + Widget _buildPendingUserInfoLine(BuildContext context) { + return Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onClick(widget.msg.user); + }, + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + head( + url: widget.msg.user?.userAvatar ?? "", + width: 48.w, + // headdress: msg.user?.getHeaddress()?.sourceUrl, + ), + Positioned( + child: msgRoleTag(widget.msg.role ?? "", width: 14.w), + bottom: 2.w, + right: 2.w, + ), + ], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + getWealthLevel( + widget.msg.user?.wealthLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + getUserLevel( + widget.msg.user?.charmLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 120.w, + fontWeight: FontWeight.w500, + widget.msg.user?.userNickname ?? "", + fontSize: 13.sp, + type: widget.msg.user?.getVIP()?.name ?? "", + needScroll: + (widget.msg.user?.userNickname?.characters.length ?? + 0) > + 10, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.msg.user?.getVIP() != null + ? netImage( + url: widget.msg.user?.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + _buildMedals( + (widget.msg.user?.wearBadge?.where((item) { + return item.use ?? false; + }).toList() ?? + []), + ), + ], + ), + ], + ), + ), + ], + ); + } + + _buildNewUserInfoLine(BuildContext context, SocialChatUserProfile user) { + return Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onClick(user); + }, + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + head( + url: user.userAvatar ?? "", + width: 48.w, + // headdress: msg.user?.getHeaddress()?.sourceUrl, + ), + Positioned( + child: msgRoleTag(widget.msg.role ?? "", width: 14.w), + bottom: 2.w, + right: 2.w, + ), + ], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + getWealthLevel( + user.wealthLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + getUserLevel( + user.charmLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 120.w, + fontWeight: FontWeight.w500, + user.userNickname ?? "", + fontSize: 13.sp, + type: user.getVIP()?.name ?? "", + needScroll: + (user.userNickname?.characters.length ?? 0) > 10, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + user.getVIP() != null + ? netImage( + url: user.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + _buildMedals( + user.wearBadge?.where((item) { + return item.use ?? false; + }).toList() ?? + [], + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +///消息 +class Msg { + String? groupId; + String? msg; + MicRes? userWheat; + String? type; + SocialChatUserProfile? user; + String? role = ""; + SocialChatGiftRes? gift; + + //礼物个数 + num? number; + + // 自定义动画需要播放的次数 + int? customAnimationCount; + + num? awardAmount; + + // 送礼给谁 + SocialChatUserProfile? toUser; + int? time = 0; + + //动画是否已经播放(骰子、石头剪刀布、随机数字) + bool? hasPlayedAnimation = false; + + bool? needUpDataUserInfo = false; + + Msg({ + required this.groupId, + required this.msg, + required this.type, + this.user, + this.toUser, + this.userWheat, + this.role = "", + this.gift, + this.number, + this.customAnimationCount, + this.awardAmount, + this.hasPlayedAnimation, + this.needUpDataUserInfo, + }) { + time = DateTime.now().millisecondsSinceEpoch; + } + + Msg.fromJson(dynamic json) { + groupId = json['groupId']; + msg = json['msg']; + type = json['type']; + role = json['role']; + time = json['time']; + number = json['number']; + customAnimationCount = json['customAnimationCount']; + awardAmount = json['awardAmount']; + hasPlayedAnimation = json['hasPlayedAnimation']; + needUpDataUserInfo = json['needUpDataUserInfo']; + userWheat = + json['userWheat'] != null ? MicRes.fromJson(json['userWheat']) : null; + user = + json['user'] != null + ? SocialChatUserProfile.fromJson(json['user']) + : null; + toUser = + json['toUser'] != null + ? SocialChatUserProfile.fromJson(json['toUser']) + : null; + + gift = + json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null; + } + + Map toJson() { + final map = {}; + map['groupId'] = groupId; + map['msg'] = msg; + map['type'] = type; + map['role'] = role; + map['time'] = time; + map['awardAmount'] = awardAmount; + map['number'] = number; + map['customAnimationCount'] = customAnimationCount; + map['hasPlayedAnimation'] = hasPlayedAnimation; + map['needUpDataUserInfo'] = needUpDataUserInfo; + if (userWheat != null) { + map['userWheat'] = userWheat?.toJson(); + } + if (user != null) { + map['user'] = user?.toJson(); + } + if (toUser != null) { + map['toUser'] = toUser?.toJson(); + } + if (gift != null) { + map['gift'] = gift?.toJson(); + } + return map; + } +} diff --git a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart index 3d65a13..390d190 100644 --- a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart +++ b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart @@ -13,6 +13,8 @@ class RoomSeatWidget extends StatefulWidget { } class _RoomSeatWidgetState extends State { + static const int _defaultVoiceRoomSeatCount = 10; + int _lastSeatCount = 0; int _normalizeSeatCount(int? seatCount) { @@ -40,19 +42,40 @@ class _RoomSeatWidgetState extends State { if (snapshot.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) { return _lastSeatCount; } + if (snapshot.hasCurrentRoom && seatCount == 0) { + return _lastSeatCount > 0 ? _lastSeatCount : _defaultVoiceRoomSeatCount; + } return seatCount; } + int _seatCountFromMicIndexes(Iterable micIndexes) { + var maxIndex = -1; + var count = 0; + for (final micIndex in micIndexes) { + count += 1; + final resolvedIndex = micIndex.toInt(); + if (resolvedIndex > maxIndex) { + maxIndex = resolvedIndex; + } + } + final indexedSeatCount = _normalizeSeatCount(maxIndex + 1); + if (indexedSeatCount > 0) { + return indexedSeatCount; + } + return _normalizeSeatCount(count); + } + @override Widget build(BuildContext context) { return Selector( selector: (context, provider) => _RoomSeatLayoutSnapshot( - seatCount: provider.roomWheatMap.length, + seatCount: _seatCountFromMicIndexes(provider.roomWheatMap.keys), configuredSeatCount: provider.currenRoom?.roomProfile?.roomSetting?.mikeSize ?.toInt() ?? 0, + hasCurrentRoom: provider.currenRoom != null, isExitingCurrentVoiceRoomSession: provider.isExitingCurrentVoiceRoomSession, ), @@ -211,11 +234,13 @@ class _RoomSeatLayoutSnapshot { const _RoomSeatLayoutSnapshot({ required this.seatCount, required this.configuredSeatCount, + required this.hasCurrentRoom, required this.isExitingCurrentVoiceRoomSession, }); final int seatCount; final int configuredSeatCount; + final bool hasCurrentRoom; final bool isExitingCurrentVoiceRoomSession; @override @@ -226,6 +251,7 @@ class _RoomSeatLayoutSnapshot { return other is _RoomSeatLayoutSnapshot && other.seatCount == seatCount && other.configuredSeatCount == configuredSeatCount && + other.hasCurrentRoom == hasCurrentRoom && other.isExitingCurrentVoiceRoomSession == isExitingCurrentVoiceRoomSession; } @@ -234,6 +260,7 @@ class _RoomSeatLayoutSnapshot { int get hashCode => Object.hash( seatCount, configuredSeatCount, + hasCurrentRoom, isExitingCurrentVoiceRoomSession, ); } diff --git a/pubspec.yaml b/pubspec.yaml index f4697b6..d413af6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -161,6 +161,7 @@ flutter: - sc_images/daily_sign_in/ - sc_images/register_reward/ - sc_images/room/ + - sc_images/room/emoji/ - sc_images/room/entrance/ - sc_images/room/anim/ - sc_images/room/anim/gift/ @@ -170,4 +171,5 @@ flutter: - sc_images/msg/ - sc_images/level/ - sc_images/coupon/ + - sc_images/vip/ - fonts/ diff --git a/sc_images/general/sc_icon_wallet_coin.png b/sc_images/general/sc_icon_wallet_coin.png new file mode 100644 index 0000000..ef68200 Binary files /dev/null and b/sc_images/general/sc_icon_wallet_coin.png differ diff --git a/sc_images/room/emoji/fluent_emoji_01.gif b/sc_images/room/emoji/fluent_emoji_01.gif new file mode 100644 index 0000000..bc066aa Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_01.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_01.webp b/sc_images/room/emoji/fluent_emoji_01.webp new file mode 100644 index 0000000..e3305f8 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_01.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_02.gif b/sc_images/room/emoji/fluent_emoji_02.gif new file mode 100644 index 0000000..7469b28 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_02.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_02.webp b/sc_images/room/emoji/fluent_emoji_02.webp new file mode 100644 index 0000000..bf33474 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_02.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_03.gif b/sc_images/room/emoji/fluent_emoji_03.gif new file mode 100644 index 0000000..faef637 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_03.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_03.webp b/sc_images/room/emoji/fluent_emoji_03.webp new file mode 100644 index 0000000..2362db1 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_03.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_04.gif b/sc_images/room/emoji/fluent_emoji_04.gif new file mode 100644 index 0000000..a5f9d56 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_04.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_04.webp b/sc_images/room/emoji/fluent_emoji_04.webp new file mode 100644 index 0000000..d7e3c1e Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_04.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_05.gif b/sc_images/room/emoji/fluent_emoji_05.gif new file mode 100644 index 0000000..163d39f Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_05.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_05.webp b/sc_images/room/emoji/fluent_emoji_05.webp new file mode 100644 index 0000000..eec2c5c Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_05.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_06.gif b/sc_images/room/emoji/fluent_emoji_06.gif new file mode 100644 index 0000000..9480e9a Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_06.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_06.webp b/sc_images/room/emoji/fluent_emoji_06.webp new file mode 100644 index 0000000..688bac2 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_06.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_07.gif b/sc_images/room/emoji/fluent_emoji_07.gif new file mode 100644 index 0000000..3ca028a Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_07.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_07.webp b/sc_images/room/emoji/fluent_emoji_07.webp new file mode 100644 index 0000000..8903dcc Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_07.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_08.gif b/sc_images/room/emoji/fluent_emoji_08.gif new file mode 100644 index 0000000..481def1 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_08.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_08.webp b/sc_images/room/emoji/fluent_emoji_08.webp new file mode 100644 index 0000000..0bda497 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_08.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_09.gif b/sc_images/room/emoji/fluent_emoji_09.gif new file mode 100644 index 0000000..0d8d2fe Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_09.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_09.webp b/sc_images/room/emoji/fluent_emoji_09.webp new file mode 100644 index 0000000..2fbfa4d Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_09.webp differ diff --git a/sc_images/room/emoji/fluent_emoji_10.gif b/sc_images/room/emoji/fluent_emoji_10.gif new file mode 100644 index 0000000..64e73d0 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_10.gif differ diff --git a/sc_images/room/emoji/fluent_emoji_10.webp b/sc_images/room/emoji/fluent_emoji_10.webp new file mode 100644 index 0000000..d285842 Binary files /dev/null and b/sc_images/room/emoji/fluent_emoji_10.webp differ diff --git a/sc_images/room/emoji/monkey_1.gif b/sc_images/room/emoji/monkey_1.gif new file mode 100644 index 0000000..dab9126 Binary files /dev/null and b/sc_images/room/emoji/monkey_1.gif differ diff --git a/sc_images/room/emoji/monkey_10.gif b/sc_images/room/emoji/monkey_10.gif new file mode 100644 index 0000000..368fe9c Binary files /dev/null and b/sc_images/room/emoji/monkey_10.gif differ diff --git a/sc_images/room/emoji/monkey_2.gif b/sc_images/room/emoji/monkey_2.gif new file mode 100644 index 0000000..8af84dd Binary files /dev/null and b/sc_images/room/emoji/monkey_2.gif differ diff --git a/sc_images/room/emoji/monkey_3.gif b/sc_images/room/emoji/monkey_3.gif new file mode 100644 index 0000000..6c58669 Binary files /dev/null and b/sc_images/room/emoji/monkey_3.gif differ diff --git a/sc_images/room/emoji/monkey_4.gif b/sc_images/room/emoji/monkey_4.gif new file mode 100644 index 0000000..5face78 Binary files /dev/null and b/sc_images/room/emoji/monkey_4.gif differ diff --git a/sc_images/room/emoji/monkey_5.gif b/sc_images/room/emoji/monkey_5.gif new file mode 100644 index 0000000..503152a Binary files /dev/null and b/sc_images/room/emoji/monkey_5.gif differ diff --git a/sc_images/room/emoji/monkey_6.gif b/sc_images/room/emoji/monkey_6.gif new file mode 100644 index 0000000..a141aa8 Binary files /dev/null and b/sc_images/room/emoji/monkey_6.gif differ diff --git a/sc_images/room/emoji/monkey_7.gif b/sc_images/room/emoji/monkey_7.gif new file mode 100644 index 0000000..529526f Binary files /dev/null and b/sc_images/room/emoji/monkey_7.gif differ diff --git a/sc_images/room/emoji/monkey_8.gif b/sc_images/room/emoji/monkey_8.gif new file mode 100644 index 0000000..cca13b7 Binary files /dev/null and b/sc_images/room/emoji/monkey_8.gif differ diff --git a/sc_images/room/emoji/monkey_9.gif b/sc_images/room/emoji/monkey_9.gif new file mode 100644 index 0000000..c4ee4d3 Binary files /dev/null and b/sc_images/room/emoji/monkey_9.gif differ diff --git a/sc_images/room/sc_icon_room_bottom_emoji.png b/sc_images/room/sc_icon_room_bottom_emoji.png new file mode 100644 index 0000000..8c3311f Binary files /dev/null and b/sc_images/room/sc_icon_room_bottom_emoji.png differ diff --git a/sc_images/vip/sc_vip_activate_button_bg.png b/sc_images/vip/sc_vip_activate_button_bg.png new file mode 100644 index 0000000..fc965ca Binary files /dev/null and b/sc_images/vip/sc_vip_activate_button_bg.png differ diff --git a/sc_images/vip/sc_vip_badge_1.png b/sc_images/vip/sc_vip_badge_1.png new file mode 100644 index 0000000..e9fc7ed Binary files /dev/null and b/sc_images/vip/sc_vip_badge_1.png differ diff --git a/sc_images/vip/sc_vip_badge_2.png b/sc_images/vip/sc_vip_badge_2.png new file mode 100644 index 0000000..7c7d046 Binary files /dev/null and b/sc_images/vip/sc_vip_badge_2.png differ diff --git a/sc_images/vip/sc_vip_badge_3.png b/sc_images/vip/sc_vip_badge_3.png new file mode 100644 index 0000000..7b254f0 Binary files /dev/null and b/sc_images/vip/sc_vip_badge_3.png differ diff --git a/sc_images/vip/sc_vip_badge_4.png b/sc_images/vip/sc_vip_badge_4.png new file mode 100644 index 0000000..db52a33 Binary files /dev/null and b/sc_images/vip/sc_vip_badge_4.png differ diff --git a/sc_images/vip/sc_vip_badge_5.png b/sc_images/vip/sc_vip_badge_5.png new file mode 100644 index 0000000..ee7a968 Binary files /dev/null and b/sc_images/vip/sc_vip_badge_5.png differ diff --git a/sc_images/vip/sc_vip_card_bg_1.png b/sc_images/vip/sc_vip_card_bg_1.png new file mode 100644 index 0000000..7533a1b Binary files /dev/null and b/sc_images/vip/sc_vip_card_bg_1.png differ diff --git a/sc_images/vip/sc_vip_card_bg_2.png b/sc_images/vip/sc_vip_card_bg_2.png new file mode 100644 index 0000000..8d0d20b Binary files /dev/null and b/sc_images/vip/sc_vip_card_bg_2.png differ diff --git a/sc_images/vip/sc_vip_card_bg_3.png b/sc_images/vip/sc_vip_card_bg_3.png new file mode 100644 index 0000000..a7661ca Binary files /dev/null and b/sc_images/vip/sc_vip_card_bg_3.png differ diff --git a/sc_images/vip/sc_vip_card_bg_4.png b/sc_images/vip/sc_vip_card_bg_4.png new file mode 100644 index 0000000..d00b503 Binary files /dev/null and b/sc_images/vip/sc_vip_card_bg_4.png differ diff --git a/sc_images/vip/sc_vip_card_bg_5.png b/sc_images/vip/sc_vip_card_bg_5.png new file mode 100644 index 0000000..36f9eb8 Binary files /dev/null and b/sc_images/vip/sc_vip_card_bg_5.png differ diff --git a/sc_images/vip/sc_vip_card_ornament_1.png b/sc_images/vip/sc_vip_card_ornament_1.png new file mode 100644 index 0000000..eda5414 Binary files /dev/null and b/sc_images/vip/sc_vip_card_ornament_1.png differ diff --git a/sc_images/vip/sc_vip_card_ornament_2.png b/sc_images/vip/sc_vip_card_ornament_2.png new file mode 100644 index 0000000..6d06d88 Binary files /dev/null and b/sc_images/vip/sc_vip_card_ornament_2.png differ diff --git a/sc_images/vip/sc_vip_card_ornament_3.png b/sc_images/vip/sc_vip_card_ornament_3.png new file mode 100644 index 0000000..c2b80cd Binary files /dev/null and b/sc_images/vip/sc_vip_card_ornament_3.png differ diff --git a/sc_images/vip/sc_vip_card_ornament_4.png b/sc_images/vip/sc_vip_card_ornament_4.png new file mode 100644 index 0000000..b48ccec Binary files /dev/null and b/sc_images/vip/sc_vip_card_ornament_4.png differ diff --git a/sc_images/vip/sc_vip_card_ornament_5.png b/sc_images/vip/sc_vip_card_ornament_5.png new file mode 100644 index 0000000..177edb2 Binary files /dev/null and b/sc_images/vip/sc_vip_card_ornament_5.png differ diff --git a/sc_images/vip/sc_vip_entry_bg.png b/sc_images/vip/sc_vip_entry_bg.png new file mode 100644 index 0000000..d6b5552 Binary files /dev/null and b/sc_images/vip/sc_vip_entry_bg.png differ diff --git a/sc_images/vip/sc_vip_letter_i.png b/sc_images/vip/sc_vip_letter_i.png new file mode 100644 index 0000000..8163905 Binary files /dev/null and b/sc_images/vip/sc_vip_letter_i.png differ diff --git a/sc_images/vip/sc_vip_letter_p.png b/sc_images/vip/sc_vip_letter_p.png new file mode 100644 index 0000000..6294b97 Binary files /dev/null and b/sc_images/vip/sc_vip_letter_p.png differ diff --git a/sc_images/vip/sc_vip_letter_v.png b/sc_images/vip/sc_vip_letter_v.png new file mode 100644 index 0000000..1b146b9 Binary files /dev/null and b/sc_images/vip/sc_vip_letter_v.png differ diff --git a/sc_images/vip/sc_vip_number_1.png b/sc_images/vip/sc_vip_number_1.png new file mode 100644 index 0000000..fcac4bf Binary files /dev/null and b/sc_images/vip/sc_vip_number_1.png differ diff --git a/sc_images/vip/sc_vip_number_1_filled.png b/sc_images/vip/sc_vip_number_1_filled.png new file mode 100644 index 0000000..8dbaaee Binary files /dev/null and b/sc_images/vip/sc_vip_number_1_filled.png differ diff --git a/sc_images/vip/sc_vip_number_2.png b/sc_images/vip/sc_vip_number_2.png new file mode 100644 index 0000000..ccd33d9 Binary files /dev/null and b/sc_images/vip/sc_vip_number_2.png differ diff --git a/sc_images/vip/sc_vip_number_2_filled.png b/sc_images/vip/sc_vip_number_2_filled.png new file mode 100644 index 0000000..b800167 Binary files /dev/null and b/sc_images/vip/sc_vip_number_2_filled.png differ diff --git a/sc_images/vip/sc_vip_number_3.png b/sc_images/vip/sc_vip_number_3.png new file mode 100644 index 0000000..ccfe319 Binary files /dev/null and b/sc_images/vip/sc_vip_number_3.png differ diff --git a/sc_images/vip/sc_vip_number_3_filled.png b/sc_images/vip/sc_vip_number_3_filled.png new file mode 100644 index 0000000..d2542d3 Binary files /dev/null and b/sc_images/vip/sc_vip_number_3_filled.png differ diff --git a/sc_images/vip/sc_vip_number_4.png b/sc_images/vip/sc_vip_number_4.png new file mode 100644 index 0000000..7a17d59 Binary files /dev/null and b/sc_images/vip/sc_vip_number_4.png differ diff --git a/sc_images/vip/sc_vip_number_4_filled.png b/sc_images/vip/sc_vip_number_4_filled.png new file mode 100644 index 0000000..ae60276 Binary files /dev/null and b/sc_images/vip/sc_vip_number_4_filled.png differ diff --git a/sc_images/vip/sc_vip_number_5.png b/sc_images/vip/sc_vip_number_5.png new file mode 100644 index 0000000..e628e70 Binary files /dev/null and b/sc_images/vip/sc_vip_number_5.png differ diff --git a/sc_images/vip/sc_vip_number_5_filled.png b/sc_images/vip/sc_vip_number_5_filled.png new file mode 100644 index 0000000..5a58075 Binary files /dev/null and b/sc_images/vip/sc_vip_number_5_filled.png differ diff --git a/sc_images/vip/sc_vip_page_bg.png b/sc_images/vip/sc_vip_page_bg.png new file mode 100644 index 0000000..e37a18f Binary files /dev/null and b/sc_images/vip/sc_vip_page_bg.png differ diff --git a/sc_images/vip/sc_vip_question.png b/sc_images/vip/sc_vip_question.png new file mode 100644 index 0000000..ac441ac Binary files /dev/null and b/sc_images/vip/sc_vip_question.png differ diff --git a/sc_images/vip/sc_vip_selected_line.png b/sc_images/vip/sc_vip_selected_line.png new file mode 100644 index 0000000..88890da Binary files /dev/null and b/sc_images/vip/sc_vip_selected_line.png differ diff --git a/sc_images/vip/sc_vip_title_left.png b/sc_images/vip/sc_vip_title_left.png new file mode 100644 index 0000000..637c357 Binary files /dev/null and b/sc_images/vip/sc_vip_title_left.png differ diff --git a/sc_images/vip/sc_vip_title_right.png b/sc_images/vip/sc_vip_title_right.png new file mode 100644 index 0000000..e5a6e1c Binary files /dev/null and b/sc_images/vip/sc_vip_title_right.png differ diff --git a/sc_images/vip/sc_vip_top_border_1.png b/sc_images/vip/sc_vip_top_border_1.png new file mode 100644 index 0000000..a9796c5 Binary files /dev/null and b/sc_images/vip/sc_vip_top_border_1.png differ diff --git a/sc_images/vip/sc_vip_top_border_2.png b/sc_images/vip/sc_vip_top_border_2.png new file mode 100644 index 0000000..dd54a9a Binary files /dev/null and b/sc_images/vip/sc_vip_top_border_2.png differ diff --git a/sc_images/vip/sc_vip_top_border_3.png b/sc_images/vip/sc_vip_top_border_3.png new file mode 100644 index 0000000..47aeab4 Binary files /dev/null and b/sc_images/vip/sc_vip_top_border_3.png differ diff --git a/sc_images/vip/sc_vip_top_border_4.png b/sc_images/vip/sc_vip_top_border_4.png new file mode 100644 index 0000000..89d3903 Binary files /dev/null and b/sc_images/vip/sc_vip_top_border_4.png differ diff --git a/sc_images/vip/sc_vip_top_border_5.png b/sc_images/vip/sc_vip_top_border_5.png new file mode 100644 index 0000000..bbcf13c Binary files /dev/null and b/sc_images/vip/sc_vip_top_border_5.png differ diff --git a/sc_images/vip/sc_vip_update_button_bg.png b/sc_images/vip/sc_vip_update_button_bg.png new file mode 100644 index 0000000..48d97a8 Binary files /dev/null and b/sc_images/vip/sc_vip_update_button_bg.png differ diff --git a/test/room_seat_widget_test.dart b/test/room_seat_widget_test.dart index 19fe25b..ab814ca 100644 --- a/test/room_seat_widget_test.dart +++ b/test/room_seat_widget_test.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:yumi/modules/room/seat/sc_seat_item.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart'; import 'package:yumi/ui_kit/widgets/room/seat/room_seat_widget.dart'; @@ -39,4 +40,60 @@ void main() { expect(find.byType(SCSeatItem), findsNWidgets(10)); }, ); + + testWidgets('RoomSeatWidget derives seat count from highest mic index', ( + tester, + ) async { + await tester.binding.setSurfaceSize(const Size(1600, 1200)); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + + final rtcProvider = + RealTimeCommunicationManager() + ..currenRoom = JoinRoomRes(roomProfile: RoomProfile()) + ..roomWheatMap = {19: MicRes(micIndex: 19)}; + + await tester.pumpWidget( + ScreenUtilInit( + designSize: const Size(375, 812), + builder: + (_, __) => + ChangeNotifierProvider.value( + value: rtcProvider, + child: MaterialApp(home: Scaffold(body: RoomSeatWidget())), + ), + ), + ); + + expect(find.byType(SCSeatItem), findsNWidgets(20)); + }); + + testWidgets( + 'RoomSeatWidget keeps default room seats visible while room data catches up', + (tester) async { + await tester.binding.setSurfaceSize(const Size(1600, 1200)); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + + final rtcProvider = + RealTimeCommunicationManager() + ..currenRoom = JoinRoomRes(roomProfile: RoomProfile()); + + await tester.pumpWidget( + ScreenUtilInit( + designSize: const Size(375, 812), + builder: + (_, __) => + ChangeNotifierProvider.value( + value: rtcProvider, + child: MaterialApp(home: Scaffold(body: RoomSeatWidget())), + ), + ), + ); + + expect(find.byType(SCSeatItem), findsNWidgets(10)); + }, + ); } diff --git a/需求进度.md b/需求进度.md index 807b815..42c5966 100644 --- a/需求进度.md +++ b/需求进度.md @@ -9,6 +9,8 @@ - 自定义背景上传页也已完成首版 UI 骨架:顶部为返回键和纯文本标题 `自定义背景图`,中间包含上传框、提示文案、示例区和底部 `Save` 按钮。当前已先接上本地相册选图和回传路径,保存后会把刚选的本地图先带回背景选择页,并插入到 `我的` tab 里做本地预览,先把交互骨架跑通。 - 已按 2026-04-22 最新页面微调继续收口交互细节:背景选择页底部新增了和上传页 `Save` 同风格的悬浮 `add` 按钮,但当前只会在 `Mine` tab 下显示,点击效果与右上角 `Custom` 一致,都会进入上传页;同时 `Official / Mine` tab 的点击波浪反馈已去掉,避免和目标稿风格不一致。 - 已继续统一两页正文区底色:当前背景选择页和背景上传页除顶部导航栏外的正文区域,都固定使用 `#0F0F0F`,不再透出全页背景图;顶部导航区域仍保留现有视觉,以便后续继续微调头部样式。 +- 已按最新反馈重做 `RoomBackgroundPreviewPage`:背景选择页点击背景卡片会进入独立预览页,底层显示当前卡片背景图,上层改为复用/贴近现有语音房头部、在线用户条、底部操作栏的真实 UI 骨架;麦位区固定为现有语音房同款 10 麦位布局,并省略礼物/SVGA 等动效层,避免预览页出现布局溢出。底部继续提供 `End Preview` 与 `Select again` 两个按钮;`End Preview` 返回后会把当前卡片标记为使用中,`Select again` 则仅返回选择页继续挑选。 +- 已继续把语音房消息区的 `All / Chat / Gift` 三个图片 tab 补进背景预览页:当前使用正式房间里的 `sc_icon_room_chat_tab_*` 选中/未选中素材,默认选中 `All`,点击可在预览页内切换 tab 高亮,但不接真实消息列表数据,避免预览页触发房间聊天逻辑。 - 当前这一步仍是“框架版”,还没有正式接入背景后台列表、我的背景接口、上传审核接口和房间背景实际应用逻辑;下一步会在现有骨架上继续把真实数据源、选中态持久化和房内背景切换接进来。 ## 本轮靓号动效替换(已完成) @@ -97,6 +99,7 @@ - 已按 2026-04-23 最新验收继续收口幸运礼物顶部横幅倍数的“时机不同步”问题:当前已撤回外层独立叠层做法,重新把倍数挂回 `SVGA` 主体条带自身的动态图层坐标系里,优先保证它和横幅本体共用同一条进场/位移时间轴,不再出现横幅在飘、倍数单独漂在上方的现象;礼物图标则继续维持 `36` 视觉尺寸并贴近 `from` 后侧。 - 已按 2026-04-23 最新验收继续微调幸运礼物顶部横幅右侧倍数的排版对齐:当前不再让上方数字和下方 `times` 各自按右边缘贴齐,而是改为共用同一条水平中心线绘制,优先修正“数字偏右、和下方文案看起来不在一列”的问题。 - 已按 2026-04-23 验收收尾要求移除房间右侧的幸运礼物临时验收入口:此前仅用于本地预览顶部 lucky gift 横幅的“测”按钮和本地造消息逻辑已从 `room_play_widget.dart / room_game_bottom_sheet.dart` 删除,房间侧边区恢复只保留正式游戏入口;幸运礼物横幅本身的 `SVGA`、动态头像/文案/倍数链路不做回退或改动。 +- 已按 2026-04-24 最新验收需求把幸运礼物 `luck_gift_reward_burst.svga` 拆到可接动态层:当前确认 `avatar / digit01 / digit02` 分别是头像、金币数额、倍数的透明占位层,并已改为在 `SVGA` 自身时间轴内同步注入当前中奖头像、金币数额和 `x倍数`,同时移除此前外层 Flutter 叠加的头像/金额/倍数,避免与 `burst` 动画不同步;验收完成后,房间右侧临时“测”按钮、本地 preview 事件和 RTM 临时 preview 方法已删除,正式幸运礼物触发链路保持不变。 - 已按 2026-04-21 最新特效需求继续调整幸运礼物 `burst`:在 `luck_gift_reward_burst.svga` 下方那块淡色圆角矩形区域,补上了与 `SVGA` 同步进场/消失的中奖倍数文案;当前会和中央金额文本共用同一显隐时机,并在 `multiple > 0` 时展示为 `xN`,避免出现 `x0` 这类无效信息。 - 已继续按 2026-04-21 最新特效稿给幸运礼物 `burst` 补上中奖发送者头像:当前头像会出现在你标的黑块位置附近,并和 `SVGA` 使用同一套开始/结束显隐时机同步出现与消失;头像显示只读取当前中奖事件的 `userAvatar`,不改现有 `burst` 触发条件和播放时长。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 @@ -250,6 +253,7 @@ - `lib/modules/room/voice_room_page.dart` - `lib/modules/room/voice_room_route.dart` - `lib/modules/room/background/room_background_select_page.dart` +- `lib/modules/room/background/room_background_preview_page.dart` - `lib/modules/room/background/room_background_upload_page.dart` - `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart` - `lib/ui_kit/widgets/room/room_menu_dialog.dart` @@ -461,6 +465,9 @@ - `a1706496a01f74d27e6c60598144cecc978934b02a87567e1ced887b5b0185d5` ## 已知问题 +- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。 +- 已按最新截图回调把语音房底部聊天胶囊底色改回原先的灰色半透明风格,同时保留短胶囊和右侧独立表情按钮;针对 gif 表情在选择栏、聊天框、麦位展示中偶发闪烁的问题,新增统一的房间表情 asset 展示组件,复用 `AssetImage`、提前预热本地 gif、用 `RepaintBoundary` 隔离重绘,并让 GridView 表情 item 保持状态,减少父组件刷新时 gif 重建和首帧跳动。 +- 已继续针对黄脸 `fluent_emoji` 表情闪烁做素材侧处理:这组 150x150 透明 GIF 的帧处置方式更容易在 Flutter 解码/循环时露底闪烁,当前已批量转成 animated WebP 并切换选择栏引用;猴子表情保持原 GIF,不改变已验证稳定的素材。 - `ios/Podfile.lock` 还保留旧插件记录,因为本轮未执行 `pod install`;但 Flutter 当前依赖链路已经不再包含已移除插件。 - universal APK 仍然会非常大;当前正确发包方式应优先使用 `--split-per-abi` 或直接产出 `aab`。 - 仍有 `12` 张 `webp` 被 TinyPNG 拒绝解码,推测是动画或特殊编码格式,不能继续直接走 TinyPNG 常规压缩。