diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart index 0c51636..0c989b6 100644 --- a/lib/modules/room/online/room_online_page.dart +++ b/lib/modules/room/online/room_online_page.dart @@ -14,6 +14,7 @@ import '../../../ui_kit/components/sc_tts.dart'; import '../../../ui_kit/components/text/sc_text.dart'; import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../../shared/tools/sc_lk_dialog_util.dart'; +import '../../../ui_kit/widgets/badge/sc_user_badge_strip.dart'; import '../../../ui_kit/widgets/room/room_user_info_card.dart'; ///房间用户在线列表 @@ -107,84 +108,105 @@ class _RoomOnlinePageState _openUserCard(userInfo); }, ), - SizedBox(width: 3.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - msgRoleTag(userInfo.roles ?? "", width: 20.w, height: 20.w), - SizedBox(width: 3.w), - socialchatNickNameText( - textColor: Colors.black, - maxWidth: 200.w, - userInfo.userNickname ?? "", - fontSize: 14.sp, - type: userInfo.getVIP()?.name ?? "", - needScroll: - (userInfo.userNickname?.characters.length ?? 0) > 16, - ), - getVIPBadge( - userInfo.getVIP()?.name, - width: 45.w, - height: 25.w, - ), - // ListView.separated( - // scrollDirection: Axis.horizontal, - // shrinkWrap: true, - // itemCount: userInfo.wearBadge?.length ?? 0, - // itemBuilder: (context, index) { - // return netImage( - // width: 25.w, - // height: 25.w, - // url: userInfo.wearBadge?[index].selectUrl ?? "", - // ); - // }, - // separatorBuilder: (BuildContext context, int index) { - // return SizedBox(width: 5.w); - // }, - // ), - ], - ), - SizedBox(height: 3.w), - GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(vertical: 3.w), - child: SCSpecialIdBadge( - idText: userInfo.getID(), - showAnimated: userInfo.hasSpecialId(), - assetPath: SCSpecialIdAssets.userId, - animationWidth: 62.w, - animationHeight: 24.w, - showTextBesideAnimated: true, - animatedTextSpacing: 0, - showAnimatedGradientText: - userInfo.shouldShowColoredSpecialIdText(), - animationTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + msgRoleTag( + userInfo.roles ?? "", + width: 20.w, + height: 20.w, ), - normalTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, + SizedBox(width: 3.w), + socialchatNickNameText( + textColor: Colors.black, + maxWidth: 132.w, + userInfo.userNickname ?? "", + fontSize: 14.sp, + type: userInfo.getVIP()?.name ?? "", + needScroll: + (userInfo.userNickname?.characters.length ?? 0) > + 16, ), - showCopyIcon: true, - showCopyIconWhenAnimated: true, - copyIconSize: 12.w, - copyIconSpacing: 5.w, - animationFit: BoxFit.contain, - ), + getVIPBadge( + userInfo.vipLevel ?? userInfo.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + SizedBox(width: 3.w), + getWealthLevel( + userInfo.wealthLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + SizedBox(width: 2.w), + getUserLevel( + userInfo.charmLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + ], ), - onTap: () { - Clipboard.setData(ClipboardData(text: userInfo.getID())); - SCTts.show( - SCAppLocalizations.of(context)!.copiedToClipboard, - ); - }, - ), - ], + SizedBox(height: 3.w), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + child: SCSpecialIdBadge( + idText: userInfo.getID(), + showAnimated: userInfo.hasSpecialId(), + assetPath: SCSpecialIdAssets.userId, + animationWidth: 62.w, + animationHeight: 24.w, + showTextBesideAnimated: true, + animatedTextSpacing: 0, + showAnimatedGradientText: + userInfo.shouldShowColoredSpecialIdText(), + animationTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + showCopyIcon: true, + showCopyIconWhenAnimated: true, + copyIconSize: 12.w, + copyIconSpacing: 5.w, + animationFit: BoxFit.contain, + ), + ), + onTap: () { + Clipboard.setData(ClipboardData(text: userInfo.getID())); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(height: 2.w), + SCUserBadgeStrip( + userId: userInfo.id, + fallbackBadges: + userInfo.wearBadge + ?.where((item) => item.use ?? false) + .toList() ?? + const [], + height: 23.w, + badgeHeight: 21.w, + longBadgeWidth: 51.w, + spacing: 4.5.w, + maxBadges: 8, + reserveSpace: true, + ), + ], + ), ), ], ), diff --git a/lib/modules/store/data_card/store_data_card_page.dart b/lib/modules/store/data_card/store_data_card_page.dart new file mode 100644 index 0000000..cd26c3d --- /dev/null +++ b/lib/modules/store/data_card/store_data_card_page.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_currency_type.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart'; +import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart'; + +class StoreDataCardPage extends SCPageList { + const StoreDataCardPage({super.key}); + + @override + StoreDataCardPageState createState() => StoreDataCardPageState(); +} + +class StoreDataCardPageState + extends SCPageListState { + double disCount = 1; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.78, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: + items.isEmpty && isLoading + ? const SCStoreGridSkeleton() + : buildList(context), + ); + } + + @override + Widget buildItem(StoreDataCardListResBean item) { + final res = item; + final prices = res.res.propsPrices ?? const []; + final firstPrice = prices.isNotEmpty ? prices.first : null; + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + res.isSelecte + ? const Color(0xff18F2B1).withValues(alpha: 0.1) + : SCGlobalConfig.businessLogicStrategy + .getStoreItemBackgroundColor(), + border: Border.all( + color: + res.isSelecte + ? SCGlobalConfig.businessLogicStrategy + .getStoreItemSelectedBorderColor() + : SCGlobalConfig.businessLogicStrategy + .getStoreItemUnselectedBorderColor(), + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: netImage( + url: res.res.propsResources?.cover ?? "", + width: 72.w, + height: 50.w, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 8.w), + buildStoreBagItemTitle( + res.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 11.sp, + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 10.w), + Expanded( + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStoreItemGoldIcon(), + width: 22.w, + ), + ), + const TextSpan(text: " "), + if (disCount < 1) + TextSpan( + text: "${firstPrice?.amount ?? 0}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + ), + if (disCount < 1) const TextSpan(text: " "), + TextSpan( + text: + "${((firstPrice?.amount ?? 0) * disCount).toInt()}/${firstPrice?.days ?? 0} ${SCAppLocalizations.of(context)!.day}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + strutStyle: const StrutStyle( + height: 1.3, + fontWeight: FontWeight.w500, + forceStrutHeight: true, + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ), + onTap: () { + _selectItem(res); + _showDetail(res.res); + }, + ); + } + + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + final beans = []; + final storeList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.DATA_CARD.name, + ); + for (final value in storeList) { + beans.add(StoreDataCardListResBean(value, false)); + } + onSuccess(beans); + } catch (_) { + onErr?.call(); + } + } + + void _showDetail(StoreListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsStoreDataCardDetailDialog(res, disCount); + }, + ); + } + + void _selectItem(StoreDataCardListResBean res) { + for (final value in items) { + value.isSelecte = false; + } + res.isSelecte = true; + setState(() {}); + } +} + +class StoreDataCardListResBean { + StoreDataCardListResBean(this.res, this.isSelecte); + + StoreListRes res; + bool isSelecte; +} diff --git a/lib/modules/store/store_page.dart b/lib/modules/store/store_page.dart index 42c09d3..9132312 100644 --- a/lib/modules/store/store_page.dart +++ b/lib/modules/store/store_page.dart @@ -1,22 +1,15 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; 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/app/constants/sc_global_config.dart'; import 'package:yumi/modules/store/store_route.dart'; -import 'package:yumi/modules/store/theme/store_theme_page.dart'; +import 'package:yumi/modules/store/data_card/store_data_card_page.dart'; import 'package:provider/provider.dart'; -import 'package:tancent_vap/utils/constant.dart'; -import 'package:tancent_vap/widgets/vap_view.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; -import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/modules/store/chatbox/store_chatbox_page.dart'; import 'package:yumi/modules/store/headdress/store_headdress_page.dart'; @@ -25,8 +18,10 @@ import 'package:yumi/modules/store/mountains/store_mountains_page.dart'; import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; class StorePage extends StatefulWidget { + const StorePage({super.key}); + @override - _StorePageState createState() => _StorePageState(); + State createState() => _StorePageState(); } class _StorePageState extends State @@ -40,16 +35,11 @@ class _StorePageState extends State void initState() { super.initState(); _pages.add(StoreHeaddressPage()); - _pages.add( - StoreMountainsPage(), - ); - _pages.add( - StoreThemePage(), - ); + _pages.add(StoreMountainsPage()); + _pages.add(const StoreDataCardPage()); _pages.add(StoreChatboxPage()); _tabController = TabController(length: _pages.length, vsync: this); - _tabController.addListener(() { - }); + _tabController.addListener(() {}); Provider.of(context, listen: false).balance(); } @@ -64,7 +54,7 @@ class _StorePageState extends State _tabs.clear(); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.headdress)); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.theme)); + _tabs.add(const Tab(text: 'Profile Card')); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); return Stack( children: [ @@ -86,10 +76,10 @@ class _StorePageState extends State padding: EdgeInsets.all(5.w), margin: EdgeInsetsDirectional.only( end: - SCGlobalConfig.businessLogicStrategy - .getStorePageShoppingBagMargin() - .end - .w, + SCGlobalConfig.businessLogicStrategy + .getStorePageShoppingBagMargin() + .end + .w, ), child: Image.asset( SCGlobalConfig.businessLogicStrategy @@ -122,12 +112,12 @@ class _StorePageState extends State unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy .getStorePageTabUnselectedLabelStyle() .copyWith( - fontSize: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabUnselectedLabelStyle() - .fontSize - ?.sp, - ), + fontSize: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .fontSize + ?.sp, + ), // indicatorPadding: EdgeInsets.symmetric( // vertical: 5.w, // horizontal: 15.w, @@ -135,12 +125,12 @@ class _StorePageState extends State indicator: SCFixedWidthTabIndicator( width: 15.w, color: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabIndicatorColor(), + SCGlobalConfig.businessLogicStrategy + .getStorePageTabIndicatorColor(), ), dividerColor: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabDividerColor(), + SCGlobalConfig.businessLogicStrategy + .getStorePageTabDividerColor(), controller: _tabController, tabs: _tabs, ), @@ -158,48 +148,48 @@ class _StorePageState extends State return GestureDetector( child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12.w), - topRight: Radius.circular(12.w), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withValues(alpha: 0.1), + ), + padding: EdgeInsets.only(bottom: 12.w, top: 12.w), + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 25.w, + height: 25.w, ), - color: Color(0xff18F2B1).withOpacity(0.1), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldTextColor(), + ), + SizedBox(width: 4.w), + Icon( + Icons.chevron_right, + size: 20.w, + color: + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIconColor(), + ), + ], + ), ), - padding: EdgeInsets.only(bottom: 12.w,top: 12.w), - child: Row( - children: [ - SizedBox(width: 10.w), - Image.asset( - SCGlobalConfig.businessLogicStrategy - .getStorePageGoldIcon(), - width: 25.w, - height: 25.w, - ), - SizedBox(width: 5.w), - text( - "${ref.myBalance}", - fontSize: 14.sp, - textColor: - SCGlobalConfig.businessLogicStrategy - .getStorePageGoldTextColor(), - ), - SizedBox(width: 4.w), - Icon( - Icons.chevron_right, - size: 20.w, - color: - SCGlobalConfig.businessLogicStrategy - .getStorePageGoldIconColor(), - ), - ], - ), - ), - onTap: () { - SCNavigatorUtils.push( - context, - WalletRoute.recharge, - replace: false, - ); - }, + onTap: () { + SCNavigatorUtils.push( + context, + WalletRoute.recharge, + replace: false, + ); + }, ); }, ), diff --git a/lib/modules/user/my_items/data_card/bags_data_card_page.dart b/lib/modules/user/my_items/data_card/bags_data_card_page.dart new file mode 100644 index 0000000..ca669b1 --- /dev/null +++ b/lib/modules/user/my_items/data_card/bags_data_card_page.dart @@ -0,0 +1,399 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/sc_tts.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/bag/props_bag_data_card_detail_dialog.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart'; + +class BagsDataCardPage extends SCPageList { + const BagsDataCardPage({super.key}); + + @override + BagsDataCardPageState createState() => BagsDataCardPageState(); +} + +class BagsDataCardPageState + extends SCPageListState { + PropsResources? myDataCard; + BagsListRes? selectedDataCard; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.88, + ); + myDataCard = AccountStorage().getDataCard(); + loadData(1); + } + + @override + Widget build(BuildContext context) { + final hasSelection = selectedDataCard != null; + final selectedExpired = + (selectedDataCard?.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch; + final selectedInUse = + myDataCard?.id == selectedDataCard?.propsResources?.id; + + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded( + child: + items.isEmpty && isLoading + ? const SCBagGridSkeleton() + : buildList(context), + ), + hasSelection ? SizedBox(height: 10.w) : const SizedBox.shrink(), + hasSelection + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: const Color(0xff18F2B1).withValues(alpha: 0.1), + ), + child: Column( + children: [ + SizedBox(height: 5.w), + Row( + spacing: 5.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${SCAppLocalizations.of(context)!.expirationTime}:", + textColor: Colors.white, + fontSize: 12.sp, + ), + CountdownTimer( + fontSize: 11.sp, + color: SocialChatTheme.primaryLight, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + selectedDataCard?.expireTime ?? 0, + ), + ), + ], + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.renewal, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + selectedInUse + ? Colors.white + : SocialChatTheme.primaryLight, + ), + child: text( + selectedExpired + ? SCAppLocalizations.of(context)!.renewal + : selectedInUse + ? SCAppLocalizations.of(context)!.inUse + : SCAppLocalizations.of(context)!.use, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: + selectedInUse + ? const Color(0xffB1B1B1) + : Colors.white, + ), + ), + onTap: () { + if (selectedExpired) { + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + if (!selectedInUse) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selectedDataCard!, false); + }, + ); + }, + ); + return; + } + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUnUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selectedDataCard!, true); + }, + ); + }, + ); + }, + ), + ], + ), + SizedBox(height: 10.w), + ], + ), + ) + : const SizedBox.shrink(), + ], + ), + ); + } + + @override + Widget buildItem(BagsListRes item) { + final res = item; + final selected = + selectedDataCard?.propsResources?.id == res.propsResources?.id; + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: const Color(0xff18F2B1).withValues(alpha: 0.1), + border: Border.all( + color: selected ? SocialChatTheme.primaryLight : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: netImage( + url: res.propsResources?.cover ?? "", + width: 72.w, + height: 50.w, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 8.w), + buildStoreBagItemTitle( + res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: Row( + children: [ + CountdownTimer( + fontSize: 9.sp, + color: Colors.white, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + res.expireTime ?? 0, + ), + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/store/sc_icon_bag_clock.png", + width: 22.w, + height: 22.w, + ), + ], + ), + ), + ), + PositionedDirectional( + bottom: 5.w, + end: 5.w, + child: GestureDetector( + onTap: () { + selectedDataCard = res; + setState(() {}); + _showDetail(res); + }, + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + child: Image.asset( + "sc_images/store/sc_icon_shop_item_search.png", + width: 18.w, + height: 18.w, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + onTap: () { + selectedDataCard = res; + setState(() {}); + }, + ); + } + + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + final storeList = await SCStoreRepositoryImp().storeBackpack( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCPropsType.DATA_CARD.name, + ); + for (final item in storeList) { + if (item.propsResources?.id == myDataCard?.id) { + selectedDataCard = item; + continue; + } + } + onSuccess(storeList); + } catch (_) { + onErr?.call(); + } + } + + void _showDetail(BagsListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsBagDataCardDetailDialog(res); + }, + onDismiss: () { + myDataCard = AccountStorage().getDataCard(); + setState(() {}); + }, + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(context: context); + final profileManager = Provider.of( + context, + listen: false, + ); + final localizations = SCAppLocalizations.of(context)!; + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.DATA_CARD.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + if (!mounted) { + SCLoadingManager.hide(); + return; + } + setState(() {}); + if (!unload) { + myDataCard = res.propsResources; + SCTts.show(localizations.successfulWear); + } else { + myDataCard = null; + SCTts.show(localizations.successfullyUnloaded); + } + profileManager.fetchUserProfileData(loadGuardCount: false); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/modules/user/my_items/my_items_page.dart b/lib/modules/user/my_items/my_items_page.dart index 13abae2..e4a7a01 100644 --- a/lib/modules/user/my_items/my_items_page.dart +++ b/lib/modules/user/my_items/my_items_page.dart @@ -6,6 +6,7 @@ import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/modules/store/store_route.dart'; import 'package:yumi/modules/user/my_items/chatbox/bags_chatbox_page.dart'; +import 'package:yumi/modules/user/my_items/data_card/bags_data_card_page.dart'; import 'package:yumi/modules/user/my_items/gift/bags_gift_page.dart'; import 'package:yumi/modules/user/my_items/headdress/bags_headdress_page.dart'; import 'package:yumi/modules/user/my_items/mountains/bags_mountains_page.dart'; @@ -33,6 +34,7 @@ class _MyItemsPageState extends State _pages.add(BagsMountainsPage()); _pages.add(BagsGiftPage()); _pages.add(BagsChatboxPage()); + _pages.add(const BagsDataCardPage()); _tabController = TabController(length: _pages.length, vsync: this); _tabController.addListener(() {}); } @@ -50,6 +52,7 @@ class _MyItemsPageState extends State _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); + _tabs.add(const Tab(text: 'Profile Card')); return Stack( children: [ Image.asset( @@ -89,39 +92,37 @@ class _MyItemsPageState extends State top: false, child: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TabBar( - tabAlignment: TabAlignment.center, - isScrollable: true, - labelColor: Colors.white, - unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy - .getStorePageTabUnselectedLabelStyle() - .copyWith( - fontSize: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabUnselectedLabelStyle() - .fontSize - ?.sp, - ), - // indicatorPadding: EdgeInsets.symmetric( - // vertical: 5.w, - // horizontal: 15.w, - // ), - indicator: SCFixedWidthTabIndicator( - width: 15.w, - color: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabIndicatorColor(), - ), - dividerColor: + SizedBox( + width: double.infinity, + child: TabBar( + tabAlignment: TabAlignment.center, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .fontSize + ?.sp, + ), + // indicatorPadding: EdgeInsets.symmetric( + // vertical: 5.w, + // horizontal: 15.w, + // ), + indicator: SCFixedWidthTabIndicator( + width: 15.w, + color: SCGlobalConfig.businessLogicStrategy - .getStorePageTabDividerColor(), - controller: _tabController, - tabs: _tabs, + .getStorePageTabIndicatorColor(), ), - ], + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabDividerColor(), + controller: _tabController, + tabs: _tabs, + ), ), SizedBox(height: 5.w), Expanded( diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index efb8533..286bb38 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -24,6 +24,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_pick_utils.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; @@ -31,6 +32,7 @@ import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart'; +import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart'; import '../../../app/constants/sc_screen.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; @@ -65,6 +67,7 @@ class _PersonDetailPageState extends State { Map counterMap = {}; List giftWallList = []; + List honorWallBadges = []; // 添加滚动控制器 final ScrollController _scrollController = ScrollController(); @@ -134,6 +137,7 @@ class _PersonDetailPageState extends State { isBlacklistLoading = false; } userCounter(widget.tageId); + _loadHonorWall(); _loadGiftWall(); } @@ -159,6 +163,26 @@ class _PersonDetailPageState extends State { .catchError((e) {}); } + void _loadHonorWall() { + final userId = widget.tageId.trim(); + if (userId.isEmpty) { + return; + } + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; + final future = + currentUserId.isNotEmpty && currentUserId == userId + ? SCAccountRepository().currentUserBadges() + : SCAccountRepository().otherUserBadges(userId); + future + .then((result) { + if (!mounted) return; + honorWallBadges = result.honorWallBadges; + setState(() {}); + }) + .catchError((e) {}); + } + @override void dispose() { _scrollController.dispose(); @@ -556,9 +580,57 @@ class _PersonDetailPageState extends State { ); } + Widget _buildProfileDataCardBackground(PropsResources? dataCard) { + if (dataCard == null) { + return const ColoredBox(color: _profileBg); + } + return Stack( + fit: StackFit.expand, + children: [ + SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.26), + _profileBg.withValues(alpha: 0.54), + _profileBg.withValues(alpha: 0.78), + ], + stops: const [0, 0.45, 1], + ), + ), + ), + ], + ); + } + + PropsResources? _activeDataCard(SocialChatUserProfile? profile) { + final now = DateTime.now().millisecondsSinceEpoch; + for (final item in profile?.useProps ?? const []) { + final resource = item.propsResources; + if (resource?.type != SCPropsType.DATA_CARD.name) { + continue; + } + final expireText = item.expireTime?.trim() ?? ""; + final expireTime = int.tryParse(expireText); + if (expireText.isNotEmpty && (expireTime ?? 0) <= now) { + continue; + } + final source = resource?.sourceUrl?.trim() ?? ""; + final cover = resource?.cover?.trim() ?? ""; + if (source.isNotEmpty || cover.isNotEmpty) { + return resource; + } + } + return null; + } + Widget _buildProfileHeader( SocialChatUserProfileManager ref, List backgroundPhotos, + PropsResources? dataCard, ) { return SizedBox( height: 520.w, @@ -610,7 +682,7 @@ class _PersonDetailPageState extends State { left: 0, right: 0, bottom: 0, - child: Container(color: _profileBg), + child: _buildProfileDataCardBackground(dataCard), ), Positioned( top: 210.w, @@ -642,7 +714,7 @@ class _PersonDetailPageState extends State { Widget _buildProfileColumnContent() { return Container( width: double.infinity, - color: _profileBg, + color: Colors.transparent, padding: EdgeInsets.fromLTRB(10.w, 4.w, 10.w, 220.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -658,8 +730,19 @@ class _PersonDetailPageState extends State { ), SizedBox(height: 12.w), _buildWallGrid( - itemCount: _wallPreviewItemCount, - itemBuilder: (_) => _buildHonorPlaceholderItem(), + itemCount: + honorWallBadges.isEmpty + ? _wallPreviewItemCount + : honorWallBadges.length + .clamp(0, _wallPreviewItemCount) + .toInt(), + itemBuilder: (index) { + final badge = + index < honorWallBadges.length + ? honorWallBadges[index] + : null; + return _buildHonorWallItem(badge); + }, ), SizedBox(height: 18.w), _buildProfileSectionTitle( @@ -744,20 +827,34 @@ class _PersonDetailPageState extends State { ); } - Widget _buildHonorPlaceholderItem() { + Widget _buildHonorWallItem(WearBadge? badge) { return _buildWallItemFrame( - child: Opacity( - opacity: 0.45, - child: Image.asset( - "sc_images/room/sc_icon_room_music_empty.png", - width: 38.w, - height: 38.w, - fit: BoxFit.contain, - ), - ), + child: + badge == null + ? Opacity( + opacity: 0.45, + child: Image.asset( + "sc_images/room/sc_icon_room_music_empty.png", + width: 38.w, + height: 38.w, + fit: BoxFit.contain, + ), + ) + : Padding( + padding: EdgeInsets.all(8.w), + child: netImage( + url: _badgeUrl(badge), + width: double.infinity, + height: double.infinity, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ), ); } + String _badgeUrl(WearBadge badge) => (badge.selectUrl ?? "").trim(); + Widget _buildGiftWallItem(SocialChatGiftRes? gift) { return _buildWallItemFrame( child: Stack( @@ -834,7 +931,7 @@ class _PersonDetailPageState extends State { left: 0, right: 0, bottom: 0, - child: Container(height: backgroundHeight, color: _profileBg), + child: SizedBox(height: backgroundHeight), ), if (hasRoom) Positioned( @@ -1080,6 +1177,8 @@ class _PersonDetailPageState extends State { : (ref.userProfile?.backgroundPhotos ?? [])) .where((t) => t.status == 1) .toList(); + final dataCard = + isProfileLoading ? null : _activeDataCard(ref.userProfile); return Directionality( textDirection: window.locale.languageCode == "ar" @@ -1090,27 +1189,38 @@ class _PersonDetailPageState extends State { child: Stack( children: [ Positioned.fill( - child: Column( + child: Stack( children: [ - SizedBox( - height: 300.w, - width: ScreenUtil().screenWidth, - child: - backgroundPhotos.isNotEmpty - ? netImage( - url: backgroundPhotos.first.url ?? "", - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ) - : Image.asset( - 'sc_images/person/sc_icon_profile_card_default_bg.png', - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), + Column( + children: [ + SizedBox( + height: 300.w, + width: ScreenUtil().screenWidth, + child: + backgroundPhotos.isNotEmpty + ? netImage( + url: backgroundPhotos.first.url ?? "", + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ) + : Image.asset( + 'sc_images/person/sc_icon_profile_card_default_bg.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + ), + const Expanded(child: ColoredBox(color: _profileBg)), + ], + ), + Positioned( + top: 250.w, + left: 0, + right: 0, + bottom: 0, + child: _buildProfileDataCardBackground(dataCard), ), - Expanded(child: Container(color: _profileBg)), ], ), ), @@ -1449,7 +1559,11 @@ class _PersonDetailPageState extends State { physics: const BouncingScrollPhysics(), child: Column( children: [ - _buildProfileHeader(ref, backgroundPhotos), + _buildProfileHeader( + ref, + backgroundPhotos, + dataCard, + ), _buildProfileColumnContent(), ], ), diff --git a/lib/modules/user/profile/profile_wall_of_honors_page.dart b/lib/modules/user/profile/profile_wall_of_honors_page.dart index 8f34016..e2cae40 100644 --- a/lib/modules/user/profile/profile_wall_of_honors_page.dart +++ b/lib/modules/user/profile/profile_wall_of_honors_page.dart @@ -20,21 +20,13 @@ class ProfileWallOfHonorsPage extends StatefulWidget { class _ProfileWallOfHonorsPageState extends State { static const Color _profileBg = Color(0xff072121); - static const Color _mutedText = Color(0xff8F9A98); - int _selectedTabIndex = 0; bool _loading = true; SCUserBadgeRes _badges = SCUserBadgeRes.empty(); - List get _honorBadges => - _badges.displayBadges.where(_isHonorBadge).toList(); + List get _wallBadges => _badges.honorWallBadges; - List get _eventBadges => - _badges.displayBadges.where(_isEventBadge).toList(); - - int get _honorCount => _honorBadges.length; - int get _eventCount => _eventBadges.length; - int get _totalCount => _honorCount + _eventCount; + int get _totalCount => _wallBadges.length; @override void initState() { @@ -110,54 +102,6 @@ class _ProfileWallOfHonorsPageState extends State { ); } - Widget _buildTabs() { - return SizedBox( - width: double.infinity, - height: 82.w, - child: Row( - children: [ - Expanded(child: _buildTab(index: 0, label: "Honors($_honorCount)")), - Expanded(child: _buildTab(index: 1, label: "Event($_eventCount)")), - ], - ), - ); - } - - Widget _buildTab({required int index, required String label}) { - final selected = _selectedTabIndex == index; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (_selectedTabIndex == index) return; - setState(() { - _selectedTabIndex = index; - }); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - label, - fontSize: 20, - textColor: selected ? Colors.white : _mutedText, - fontWeight: FontWeight.w400, - maxLines: 1, - ), - SizedBox(height: 9.w), - AnimatedContainer( - duration: const Duration(milliseconds: 160), - width: selected ? 28.w : 0, - height: 4.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4.w), - ), - ), - ], - ), - ); - } - Widget _buildEmptyState() { return SliverFillRemaining( hasScrollBody: false, @@ -194,7 +138,7 @@ class _ProfileWallOfHonorsPageState extends State { } Widget _buildBadgeGrid() { - final list = _selectedTabIndex == 0 ? _honorBadges : _eventBadges; + final list = _wallBadges; if (list.isEmpty) { return _buildEmptyState(); } @@ -253,29 +197,6 @@ class _ProfileWallOfHonorsPageState extends State { ); } - bool _isHonorBadge(WearBadge badge) { - final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; - final type = badge.type?.trim().toUpperCase() ?? ""; - return sourceType == "ACHIEVEMENT" || - sourceType == "VIP" || - (sourceType.isEmpty && - (type == "LONG" || type == "ACHIEVEMENT" || type == "VIP")); - } - - bool _isEventBadge(WearBadge badge) { - final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; - final type = badge.type?.trim().toUpperCase() ?? ""; - if (sourceType == "ACTIVITY") { - return true; - } - if (sourceType.isNotEmpty) { - return false; - } - return type == "ACTIVITY" || - type == "SHORT" || - (type != "LONG" && type != "VIP" && type != "ACHIEVEMENT"); - } - String _badgeUrl(WearBadge badge) { return (badge.selectUrl ?? "").trim(); } @@ -289,9 +210,7 @@ class _ProfileWallOfHonorsPageState extends State { child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ - SliverToBoxAdapter( - child: Column(children: [_buildTopBar(), _buildTabs()]), - ), + SliverToBoxAdapter(child: _buildTopBar()), _loading ? _buildLoadingState() : _buildBadgeGrid(), ], ), diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 6be89f2..a0ae1c6 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -851,6 +851,8 @@ class WearBadge { String? sourceType, String? type, String? userId, + int? updateTime, + String? updatedAt, bool? use, }) { _animationUrl = animationUrl; @@ -866,6 +868,8 @@ class WearBadge { _sourceType = sourceType; _type = type; _userId = userId; + _updateTime = updateTime; + _updatedAt = updatedAt; } WearBadge.fromJson(dynamic json) { @@ -889,6 +893,10 @@ class WearBadge { _stringValue(json['sourceType']) ?? _sourceTypeFromType(rawType); _type = rawType; _userId = _stringValue(json['userId']); + _updateTime = _intValue( + json['updateTime'] ?? json['updatedAt'] ?? json['createTime'], + ); + _updatedAt = _stringValue(json['updatedAt']); } String? _animationUrl; @@ -904,6 +912,8 @@ class WearBadge { String? _sourceType; String? _type; String? _userId; + int? _updateTime; + String? _updatedAt; WearBadge copyWith({ String? animationUrl, @@ -919,6 +929,8 @@ class WearBadge { String? sourceType, String? type, String? userId, + int? updateTime, + String? updatedAt, }) => WearBadge( animationUrl: animationUrl ?? _animationUrl, badgeKey: badgeKey ?? _badgeKey, @@ -933,6 +945,8 @@ class WearBadge { sourceType: sourceType ?? _sourceType, type: type ?? _type, userId: userId ?? _userId, + updateTime: updateTime ?? _updateTime, + updatedAt: updatedAt ?? _updatedAt, ); String? get animationUrl => _animationUrl; @@ -961,6 +975,10 @@ class WearBadge { String? get userId => _userId; + int? get updateTime => _updateTime; + + String? get updatedAt => _updatedAt; + Map toJson() { final map = {}; map['animationUrl'] = _animationUrl; @@ -976,6 +994,8 @@ class WearBadge { map['sourceType'] = _sourceType; map['type'] = _type; map['userId'] = _userId; + map['updateTime'] = _updateTime; + map['updatedAt'] = _updatedAt; return map; } diff --git a/lib/shared/business_logic/models/res/room_user_card_res.dart b/lib/shared/business_logic/models/res/room_user_card_res.dart index a9108f1..3210917 100644 --- a/lib/shared/business_logic/models/res/room_user_card_res.dart +++ b/lib/shared/business_logic/models/res/room_user_card_res.dart @@ -12,19 +12,20 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart'; /// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} -class RoomUserCardRes { - RoomUserCardRes({ - bool? agent, - bool? anchor, - bool? follow, - bool? friend, - String? ktvIntegral, - String? roomRole, - String? supperVip, - List? useBadge, - List? useProps, - UserLevel? userLevel, - SocialChatUserProfile? userProfile,}){ +class RoomUserCardRes { + RoomUserCardRes({ + bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile, + }) { _agent = agent; _anchor = anchor; _follow = follow; @@ -35,12 +36,8 @@ class RoomUserCardRes { _useBadge = useBadge; _useProps = useProps; _userLevel = userLevel; - final normalizedVipLevel = supperVip?.trim(); - _userProfile = - normalizedVipLevel == null || normalizedVipLevel.isEmpty - ? userProfile - : userProfile?.copyWith(vipLevel: normalizedVipLevel); -} + _userProfile = _profileWithCardHints(userProfile, supperVip, userLevel); + } RoomUserCardRes.fromJson(dynamic json) { _agent = json['agent']; @@ -49,7 +46,7 @@ class RoomUserCardRes { _friend = json['friend']; _ktvIntegral = json['ktvIntegral']; _roomRole = json['roomRole']; - _supperVip = socialChatVipLevelHintFromJson(json); + _supperVip = socialChatVipLevelHintFromJson(json); if (json['useBadge'] != null) { _useBadge = []; json['useBadge'].forEach((v) { @@ -62,15 +59,15 @@ class RoomUserCardRes { _useProps?.add(UseProps.fromJson(v)); }); } - _userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null; - _userProfile = - json['userProfile'] != null - ? socialChatUserProfileFromJsonWithVipHint(json) - : null; - final normalizedVipLevel = _supperVip?.trim(); - if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) { - _userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel); - } + _userLevel = + json['userLevel'] != null + ? UserLevel.fromJson(json['userLevel']) + : null; + _userProfile = + json['userProfile'] != null + ? socialChatUserProfileFromJsonWithVipHint(json) + : null; + _userProfile = _profileWithCardHints(_userProfile, _supperVip, _userLevel); } bool? _agent; bool? _anchor; @@ -83,29 +80,31 @@ class RoomUserCardRes { List? _useProps; UserLevel? _userLevel; SocialChatUserProfile? _userProfile; -RoomUserCardRes copyWith({ bool? agent, - bool? anchor, - bool? follow, - bool? friend, - String? ktvIntegral, - String? roomRole, - String? supperVip, - List? useBadge, - List? useProps, - UserLevel? userLevel, - SocialChatUserProfile? userProfile, -}) => RoomUserCardRes( agent: agent ?? _agent, - anchor: anchor ?? _anchor, - follow: follow ?? _follow, - friend: friend ?? _friend, - ktvIntegral: ktvIntegral ?? _ktvIntegral, - roomRole: roomRole ?? _roomRole, - supperVip: supperVip ?? _supperVip, - useBadge: useBadge ?? _useBadge, - useProps: useProps ?? _useProps, - userLevel: userLevel ?? _userLevel, - userProfile: userProfile ?? _userProfile, -); + RoomUserCardRes copyWith({ + bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile, + }) => RoomUserCardRes( + agent: agent ?? _agent, + anchor: anchor ?? _anchor, + follow: follow ?? _follow, + friend: friend ?? _friend, + ktvIntegral: ktvIntegral ?? _ktvIntegral, + roomRole: roomRole ?? _roomRole, + supperVip: supperVip ?? _supperVip, + useBadge: useBadge ?? _useBadge, + useProps: useProps ?? _useProps, + userLevel: userLevel ?? _userLevel, + userProfile: userProfile ?? _userProfile, + ); bool? get agent => _agent; bool? get anchor => _anchor; bool? get follow => _follow; @@ -140,9 +139,30 @@ RoomUserCardRes copyWith({ bool? agent, map['userProfile'] = _userProfile?.toJson(); } return map; - } - -} + } +} + +SocialChatUserProfile? _profileWithCardHints( + SocialChatUserProfile? profile, + String? supperVip, + UserLevel? userLevel, +) { + if (profile == null) { + return null; + } + var result = profile; + final normalizedVipLevel = supperVip?.trim(); + if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) { + result = result.copyWith(vipLevel: normalizedVipLevel); + } + if (userLevel?.wealthLevel != null || userLevel?.charmLevel != null) { + result = result.copyWith( + wealthLevel: userLevel?.wealthLevel ?? result.wealthLevel, + charmLevel: userLevel?.charmLevel ?? result.charmLevel, + ); + } + return result; +} /// account : "" /// accountStatus : "" @@ -179,19 +199,20 @@ RoomUserCardRes copyWith({ bool? agent, /// type : "" /// userId : 0 -class WearBadge { - WearBadge({ - String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - num? id, - num? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - num? userId,}){ +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) { _animationUrl = animationUrl; _badgeKey = badgeKey; _badgeLevel = badgeLevel; @@ -200,10 +221,10 @@ class WearBadge { _id = id; _milestone = milestone; _notSelectUrl = notSelectUrl; - _selectUrl = selectUrl; - _type = type; - _userId = userId; -} + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } WearBadge.fromJson(dynamic json) { _animationUrl = json['animationUrl']; @@ -229,29 +250,31 @@ class WearBadge { String? _selectUrl; String? _type; num? _userId; -WearBadge copyWith({ String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - num? id, - num? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - num? userId, -}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, - badgeKey: badgeKey ?? _badgeKey, - badgeLevel: badgeLevel ?? _badgeLevel, - badgeName: badgeName ?? _badgeName, - expireTime: expireTime ?? _expireTime, - id: id ?? _id, - milestone: milestone ?? _milestone, - notSelectUrl: notSelectUrl ?? _notSelectUrl, - selectUrl: selectUrl ?? _selectUrl, - type: type ?? _type, - userId: userId ?? _userId, -); + WearBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) => WearBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); String? get animationUrl => _animationUrl; String? get badgeKey => _badgeKey; num? get badgeLevel => _badgeLevel; @@ -278,17 +301,14 @@ WearBadge copyWith({ String? animationUrl, map['type'] = _type; map['userId'] = _userId; return map; - } - -} - -class UserLevel { - UserLevel({ - num? charmLevel, - num? wealthLevel,}){ - _charmLevel = charmLevel; - _wealthLevel = wealthLevel; -} + } +} + +class UserLevel { + UserLevel({num? charmLevel, num? wealthLevel}) { + _charmLevel = charmLevel; + _wealthLevel = wealthLevel; + } UserLevel.fromJson(dynamic json) { _charmLevel = json['charmLevel']; @@ -296,11 +316,10 @@ class UserLevel { } num? _charmLevel; num? _wealthLevel; -UserLevel copyWith({ num? charmLevel, - num? wealthLevel, -}) => UserLevel( charmLevel: charmLevel ?? _charmLevel, - wealthLevel: wealthLevel ?? _wealthLevel, -); + UserLevel copyWith({num? charmLevel, num? wealthLevel}) => UserLevel( + charmLevel: charmLevel ?? _charmLevel, + wealthLevel: wealthLevel ?? _wealthLevel, + ); num? get charmLevel => _charmLevel; num? get wealthLevel => _wealthLevel; @@ -309,23 +328,23 @@ UserLevel copyWith({ num? charmLevel, map['charmLevel'] = _charmLevel; map['wealthLevel'] = _wealthLevel; return map; - } - -} - -class UseBadge { - UseBadge({ - String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - String? id, - String? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - String? userId,}){ + } +} + +class UseBadge { + UseBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) { _animationUrl = animationUrl; _badgeKey = badgeKey; _badgeLevel = badgeLevel; @@ -334,10 +353,10 @@ class UseBadge { _id = id; _milestone = milestone; _notSelectUrl = notSelectUrl; - _selectUrl = selectUrl; - _type = type; - _userId = userId; -} + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } UseBadge.fromJson(dynamic json) { _animationUrl = json['animationUrl']; @@ -363,29 +382,31 @@ class UseBadge { String? _selectUrl; String? _type; String? _userId; -UseBadge copyWith({ String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - String? id, - String? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - String? userId, -}) => UseBadge( animationUrl: animationUrl ?? _animationUrl, - badgeKey: badgeKey ?? _badgeKey, - badgeLevel: badgeLevel ?? _badgeLevel, - badgeName: badgeName ?? _badgeName, - expireTime: expireTime ?? _expireTime, - id: id ?? _id, - milestone: milestone ?? _milestone, - notSelectUrl: notSelectUrl ?? _notSelectUrl, - selectUrl: selectUrl ?? _selectUrl, - type: type ?? _type, - userId: userId ?? _userId, -); + UseBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) => UseBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); String? get animationUrl => _animationUrl; String? get badgeKey => _badgeKey; num? get badgeLevel => _badgeLevel; @@ -412,6 +433,5 @@ UseBadge copyWith({ String? animationUrl, map['type'] = _type; map['userId'] = _userId; return map; - } - -} + } +} diff --git a/lib/shared/business_logic/models/res/sc_user_badge_res.dart b/lib/shared/business_logic/models/res/sc_user_badge_res.dart index 904fe23..4fbbca3 100644 --- a/lib/shared/business_logic/models/res/sc_user_badge_res.dart +++ b/lib/shared/business_logic/models/res/sc_user_badge_res.dart @@ -39,6 +39,17 @@ class SCUserBadgeRes { return badges; } + List get honorWallBadges { + final source = + shortBadges.isNotEmpty + ? shortBadges + : displayBadges.where(_isHonorWallBadge).toList(); + final result = + source.where((badge) => _badgeUrl(badge).isNotEmpty).toList(); + result.sort((a, b) => _badgeSortValue(b).compareTo(_badgeSortValue(a))); + return result; + } + static dynamic _bodyFrom(dynamic json) { if (json is Map) { final body = json['body']; @@ -92,6 +103,31 @@ class SCUserBadgeRes { type == "ACHIEVEMENT"; } + static bool _isHonorWallBadge(WearBadge badge) { + final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; + final type = badge.type?.trim().toUpperCase() ?? ""; + if (type == "LONG" || type == "VIP" || sourceType == "VIP") { + return false; + } + if (sourceType.isNotEmpty) { + return sourceType == "ACTIVITY" || sourceType == "ACHIEVEMENT"; + } + return type == "SHORT" || + type == "ACTIVITY" || + type == "ACHIEVEMENT" || + type == "HONOR_ACTIVITY" || + type == "HONOR_ADMIN"; + } + + static int _badgeSortValue(WearBadge badge) { + return badge.updateTime ?? + badge.expireTime ?? + DateTime.tryParse( + (badge.updatedAt ?? "").replaceFirst(" ", "T"), + )?.millisecondsSinceEpoch ?? + 0; + } + static String? _stringValue(dynamic value) { final text = value?.toString().trim(); return text == null || text.isEmpty ? null : text; diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart index 5bffa13..e97585c 100644 --- a/lib/ui_kit/components/sc_compontent.dart +++ b/lib/ui_kit/components/sc_compontent.dart @@ -471,7 +471,43 @@ _buildPhoneInput( ///vip标识 getVIPBadge(String? type, {double? width, double? height}) { - return Container(); + final level = _vipBadgeLevel(type); + if (level <= 0) { + return const SizedBox.shrink(); + } + final assetLevel = level.clamp(1, 5).toInt(); + return Image.asset( + "sc_images/vip/sc_vip_badge_$assetLevel.png", + width: width, + height: height, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ); +} + +int _vipBadgeLevel(String? type) { + final text = type?.trim() ?? ""; + if (text.isEmpty) { + return 0; + } + final plain = int.tryParse(text); + if ((plain ?? 0) > 0) { + return plain!; + } + final vipMatch = RegExp( + r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*([1-9]\d*)', + caseSensitive: false, + ).firstMatch(text); + if (vipMatch != null) { + return int.tryParse(vipMatch.group(1) ?? "") ?? 0; + } + final trailingMatch = RegExp(r'([1-9]\d*)$').firstMatch(text); + if (trailingMatch != null) { + return int.tryParse(trailingMatch.group(1) ?? "") ?? 0; + } + return RegExp(r'S?VIP|NOBLE[_\s-]*VIP', caseSensitive: false).hasMatch(text) + ? 1 + : 0; } ///用户等级 diff --git a/lib/ui_kit/widgets/bag/props_bag_data_card_detail_dialog.dart b/lib/ui_kit/widgets/bag/props_bag_data_card_detail_dialog.dart new file mode 100644 index 0000000..37122dd --- /dev/null +++ b/lib/ui_kit/widgets/bag/props_bag_data_card_detail_dialog.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_tts.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/props/sc_data_card_resource_view.dart'; + +class PropsBagDataCardDetailDialog extends StatefulWidget { + const PropsBagDataCardDetailDialog(this.res, {super.key}); + + final BagsListRes res; + + @override + State createState() => + _PropsBagDataCardDetailDialogState(); +} + +class _PropsBagDataCardDetailDialogState + extends State { + final debouncer = Debouncer(); + PropsResources? myDataCard; + + @override + void initState() { + super.initState(); + myDataCard = AccountStorage().getDataCard(); + } + + @override + Widget build(BuildContext context) { + final expired = + (widget.res.expireTime ?? 0) < DateTime.now().millisecondsSinceEpoch; + final inUse = myDataCard?.id == widget.res.propsResources?.id; + + return Container( + width: 260.w, + height: 330.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20.w), + text( + 'Profile Card', + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + SizedBox(height: 20.w), + ClipRRect( + borderRadius: BorderRadius.circular(10.w), + child: SizedBox( + width: 214.w, + height: 150.w, + child: SCDataCardResourceView( + resource: widget.res.propsResources, + fit: BoxFit.cover, + ), + ), + ), + SizedBox(height: 25.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + expired + ? SCAppLocalizations.of(context)!.renewal + : inUse + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + debouncer.debounce( + duration: const Duration(milliseconds: 350), + onDebounce: () { + if (expired) { + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + _use(widget.res, inUse); + }, + ); + }, + ), + ], + ), + Positioned( + right: 20.w, + top: 18.w, + child: GestureDetector( + child: Icon(Icons.close, size: 20.w), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + }, + ), + ), + ], + ), + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(); + final profileManager = Provider.of( + context, + listen: false, + ); + final localizations = SCAppLocalizations.of(context)!; + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.DATA_CARD.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + await profileManager.fetchUserProfileData(loadGuardCount: false); + Future.delayed(const Duration(milliseconds: 400), () { + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + }); + if (!unload) { + SCTts.show(localizations.successfulWear); + } else { + SCTts.show(localizations.successfullyUnloaded); + } + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart new file mode 100644 index 0000000..0a60271 --- /dev/null +++ b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart @@ -0,0 +1,494 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; +import 'package:video_player/video_player.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart'; + +class SCDataCardResourceView extends StatelessWidget { + const SCDataCardResourceView({ + super.key, + this.resource, + this.sourceUrl, + this.coverUrl, + this.width, + this.height, + this.fit = BoxFit.cover, + this.loop = true, + this.fallback, + }); + + final PropsResources? resource; + final String? sourceUrl; + final String? coverUrl; + final double? width; + final double? height; + final BoxFit fit; + final bool loop; + final Widget? fallback; + + static bool isSupported(String? url) { + return _isSvga(url) || + _isPag(url) || + _isVapVideo(url) || + _isVideo(url) || + _isImage(url); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final resolvedWidth = _resolvedSize(width, constraints.maxWidth); + final resolvedHeight = _resolvedSize(height, constraints.maxHeight); + final cover = _firstNonBlank([coverUrl, resource?.cover]); + final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]); + final resourceUrl = source.isNotEmpty ? source : cover; + final fallbackView = + fallback ?? + _buildImage( + cover, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + ); + + if (resourceUrl.isEmpty) { + return fallbackView; + } + if (_isSvga(resourceUrl)) { + return SCNetworkSvgaWidget( + resource: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + loop: loop, + fit: fit, + fallback: fallbackView, + ); + } + if (_isPag(resourceUrl)) { + return RoomRocketPagPreview( + resource: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + loop: loop, + fit: fit, + underlayBuilder: (_) => fallbackView, + defaultBuilder: (_) => fallbackView, + ); + } + if (_isVapVideo(resourceUrl)) { + if (defaultTargetPlatform != TargetPlatform.android) { + return fallbackView; + } + return _LoopingDataCardVapVideo( + url: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + loop: loop, + fallback: fallbackView, + ); + } + if (_isVideo(resourceUrl)) { + return _LoopingDataCardVideo( + url: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + loop: loop, + fallback: fallbackView, + ); + } + return _buildImage( + resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + fallback: fallbackView, + ); + }, + ); + } + + static double? _resolvedSize(double? fixed, double constrained) { + if (fixed != null && fixed.isFinite && fixed > 0) { + return fixed; + } + if (constrained.isFinite && constrained > 0) { + return constrained; + } + return null; + } + + static Widget _buildImage( + String url, { + double? width, + double? height, + required BoxFit fit, + Widget? fallback, + }) { + if (url.trim().isEmpty) { + return fallback ?? SizedBox(width: width, height: height); + } + return netImage( + url: url.trim(), + width: width, + height: height, + fit: fit, + noDefaultImg: fallback != null, + loadingWidget: fallback, + errorWidget: fallback, + ); + } + + static String _firstNonBlank(Iterable values) { + for (final value in values) { + final text = value?.trim() ?? ''; + if (text.isNotEmpty) { + return text; + } + } + return ''; + } + + static bool _hasExtension(String? url, List extensions) { + final lower = url?.trim().toLowerCase() ?? ''; + if (lower.isEmpty) { + return false; + } + return extensions.any( + (extension) => + lower.endsWith(extension) || + lower.contains('$extension?') || + lower.contains('$extension&'), + ); + } + + static bool _isSvga(String? url) => _hasExtension(url, const ['.svga']); + + static bool _isPag(String? url) => _hasExtension(url, const ['.pag']); + + static bool _isVapVideo(String? url) => + _hasExtension(url, const ['.mp4', '.vap']); + + static bool _isVideo(String? url) => + _hasExtension(url, const ['.mov', '.m4v']); + + static bool _isImage(String? url) => _hasExtension(url, const [ + '.png', + '.jpg', + '.jpeg', + '.webp', + '.gif', + '.bmp', + ]); +} + +class _LoopingDataCardVideo extends StatefulWidget { + const _LoopingDataCardVideo({ + required this.url, + this.width, + this.height, + required this.fit, + required this.loop, + required this.fallback, + }); + + final String url; + final double? width; + final double? height; + final BoxFit fit; + final bool loop; + final Widget fallback; + + @override + State<_LoopingDataCardVideo> createState() => _LoopingDataCardVideoState(); +} + +class _LoopingDataCardVideoState extends State<_LoopingDataCardVideo> { + VideoPlayerController? _controller; + bool _ready = false; + bool _failed = false; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void didUpdateWidget(covariant _LoopingDataCardVideo oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url || oldWidget.loop != widget.loop) { + _disposeController(); + _load(); + } + } + + Future _load() async { + final url = widget.url.trim(); + if (url.isEmpty) { + setState(() => _failed = true); + return; + } + setState(() { + _ready = false; + _failed = false; + }); + final controller = VideoPlayerController.networkUrl(Uri.parse(url)); + _controller = controller; + try { + await controller.setLooping(widget.loop); + await controller.setVolume(0); + await controller.initialize(); + if (!mounted || _controller != controller) { + await controller.dispose(); + return; + } + setState(() => _ready = true); + await controller.play(); + } catch (_) { + if (!mounted || _controller != controller) { + return; + } + setState(() { + _ready = false; + _failed = true; + }); + } + } + + void _disposeController() { + final controller = _controller; + _controller = null; + if (controller != null) { + controller.dispose(); + } + } + + @override + void dispose() { + _disposeController(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final controller = _controller; + if (_failed || !_ready || controller == null) { + return widget.fallback; + } + final videoSize = controller.value.size; + final child = + videoSize.width > 0 && videoSize.height > 0 + ? FittedBox( + fit: widget.fit, + child: SizedBox( + width: videoSize.width, + height: videoSize.height, + child: VideoPlayer(controller), + ), + ) + : VideoPlayer(controller); + return SizedBox( + width: widget.width, + height: widget.height, + child: ClipRect(child: child), + ); + } +} + +class _LoopingDataCardVapVideo extends StatefulWidget { + const _LoopingDataCardVapVideo({ + required this.url, + this.width, + this.height, + required this.fit, + required this.loop, + required this.fallback, + }); + + final String url; + final double? width; + final double? height; + final BoxFit fit; + final bool loop; + final Widget fallback; + + @override + State<_LoopingDataCardVapVideo> createState() => + _LoopingDataCardVapVideoState(); +} + +class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> { + static const int _legacyRgbLeftAlphaRightMode = 3; + + VapController? _controller; + int _playToken = 0; + bool _failed = false; + + @override + void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url || + oldWidget.loop != widget.loop || + oldWidget.fit != widget.fit) { + _playToken++; + _stop(); + if (_failed) { + setState(() => _failed = false); + } + _play(); + } + } + + @override + void dispose() { + _playToken++; + _stop(); + _controller = null; + super.dispose(); + } + + void _handleViewCreated(VapController controller) { + _controller = controller; + controller.setAnimListener( + onFailed: (_, __, ___) => _markFailed(), + onVideoStart: () { + if (mounted && _failed) { + setState(() => _failed = false); + } + }, + ); + _play(); + } + + Future _play() async { + final controller = _controller; + final url = widget.url.trim(); + if (controller == null || url.isEmpty) { + return; + } + final token = ++_playToken; + try { + final playablePath = await _resolvePlayablePath(url); + if (!_isCurrent(token, controller)) { + return; + } + await controller.stop(); + if (!_isCurrent(token, controller)) { + return; + } + await controller.setMute(true); + await controller.enableVersion1(true); + await controller.setVideoMode(_legacyRgbLeftAlphaRightMode); + await controller.setLoop(widget.loop ? -1 : 0); + await controller.setScaleType(_scaleTypeForFit(widget.fit).key); + if (!_isCurrent(token, controller)) { + return; + } + final pathType = SCPathUtils.getPathType(playablePath); + if (pathType == PathType.asset) { + unawaited( + controller.playAsset(playablePath).catchError((_) { + _markFailed(); + }), + ); + } else { + unawaited( + controller.playFile(_normalizeFilePath(playablePath)).catchError((_) { + _markFailed(); + }), + ); + } + } catch (_) { + if (_isCurrent(token, controller)) { + _markFailed(); + } + } + } + + bool _isCurrent(int token, VapController controller) { + return mounted && token == _playToken && identical(_controller, controller); + } + + Future _resolvePlayablePath(String url) async { + switch (SCPathUtils.getPathType(url)) { + case PathType.network: + final file = await FileCacheManager.getInstance().getFile(url: url); + return file.path; + case PathType.asset: + case PathType.file: + case PathType.unknown: + return url; + } + } + + String _normalizeFilePath(String path) { + final uri = Uri.tryParse(path); + if (uri != null && uri.scheme == 'file') { + return uri.toFilePath(); + } + return path; + } + + void _stop() { + final controller = _controller; + if (controller != null) { + unawaited(controller.stop().catchError((_) {})); + } + } + + void _markFailed() { + if (!mounted || _failed) { + return; + } + setState(() => _failed = true); + } + + ScaleType _scaleTypeForFit(BoxFit fit) { + switch (fit) { + case BoxFit.cover: + case BoxFit.fitWidth: + case BoxFit.fitHeight: + case BoxFit.none: + return ScaleType.centerCrop; + case BoxFit.fill: + return ScaleType.fitXY; + case BoxFit.contain: + case BoxFit.scaleDown: + return ScaleType.fitCenter; + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: widget.height, + child: ClipRect( + child: Stack( + fit: StackFit.expand, + children: [ + widget.fallback, + if (!_failed) + VapView( + scaleType: _scaleTypeForFit(widget.fit), + repeat: widget.loop ? -1 : 0, + mute: true, + onViewCreated: _handleViewCreated, + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index f321286..26ed376 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -1296,10 +1296,10 @@ class _MsgItemState extends State { child: SCUserBadgeStrip( userId: userId, fallbackBadges: fallbackBadges, - height: 16.w, - badgeHeight: 14.w, - longBadgeWidth: 34.w, - spacing: 3.w, + height: 23.w, + badgeHeight: 21.w, + longBadgeWidth: 51.w, + spacing: 4.5.w, maxBadges: 8, reserveSpace: true, ), @@ -1841,10 +1841,10 @@ class _MsgItemState extends State { ], ), ), - SizedBox(width: 3.w), + SizedBox(width: 8.w), Expanded( child: SizedBox( - height: 48.w, + height: 50.w, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -1887,10 +1887,10 @@ class _MsgItemState extends State { ], ), ), - SizedBox(width: 3.w), + SizedBox(width: 8.w), Expanded( child: SizedBox( - height: 48.w, + height: 50.w, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index 64bdf23..bdea8dd 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -21,6 +21,7 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart' @@ -30,6 +31,7 @@ import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/modules/gift/gift_page.dart'; import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; +import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart'; import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; @@ -83,7 +85,7 @@ class _RoomUserInfoCardState extends State { @override Widget build(BuildContext context) { - final sheetHeight = MediaQuery.of(context).size.height * 0.4; + final sheetHeight = MediaQuery.of(context).size.height * 0.6; return Consumer( builder: (context, ref, child) { @@ -151,6 +153,7 @@ class _RoomUserInfoCardState extends State { }) { final bottomPadding = MediaQuery.of(context).padding.bottom; final profile = ref.userCardInfo?.userProfile; + final dataCard = _activeDataCard(ref.userCardInfo); final canShowReport = currentUserId != widget.userId; final canShowSetting = canShowReport && @@ -162,86 +165,84 @@ class _RoomUserInfoCardState extends State { height: sheetHeight, child: ClipRRect( borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), - child: DecoratedBox( - decoration: _sheetDecoration(), - child: Stack( - children: [ - Positioned.fill( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - padding: EdgeInsets.fromLTRB( - 18.w, - 16.w, - 18.w, - bottomPadding + 10.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildProfileHeader(context, profile, ref), - SizedBox(height: 6.w), - _buildInfoChips(context, profile), - SizedBox(height: 9.w), - _buildBadgeStrip( - profile?.id ?? widget.userId, - ref, - profile, - ), - SizedBox(height: 10.w), - _buildActions( - context: context, - ref: ref, - rtcProvider: rtcProvider, - isSelf: isSelf, - canLeaveMic: canLeaveMic, - currentMicIndex: currentMicIndex, - ), - ], - ), + child: Stack( + children: [ + Positioned.fill(child: _buildSheetBackground(dataCard)), + Positioned.fill( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.fromLTRB( + 18.w, + 16.w, + 18.w, + bottomPadding + 10.w, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildProfileHeader(context, profile, ref), + SizedBox(height: 6.w), + _buildInfoChips(context, profile), + SizedBox(height: 9.w), + _buildBadgeStrip( + profile?.id ?? widget.userId, + ref, + profile, + ), + SizedBox(height: 10.w), + _buildActions( + context: context, + ref: ref, + rtcProvider: rtcProvider, + isSelf: isSelf, + canLeaveMic: canLeaveMic, + currentMicIndex: currentMicIndex, + ), + ], ), ), - if (canShowReport) - Positioned( - top: 14.w, - right: 16.w, - child: _buildHeaderIconButton( - iconAsset: "sc_images/room/sc_icon_user_card_report.png", - onTap: () { - final route = - "${SCMainRoute.report}?type=user&tageId=${widget.userId}"; - Navigator.of(context).pop(); - SCNavigatorUtils.push( - navigatorKey.currentState?.context ?? context, - route, - replace: false, - ); - }, - ), + ), + if (canShowReport) + Positioned( + top: 14.w, + right: 16.w, + child: _buildHeaderIconButton( + iconAsset: "sc_images/room/sc_icon_user_card_report.png", + onTap: () { + final route = + "${SCMainRoute.report}?type=user&tageId=${widget.userId}"; + Navigator.of(context).pop(); + SCNavigatorUtils.push( + navigatorKey.currentState?.context ?? context, + route, + replace: false, + ); + }, ), - if (canShowSetting) - Positioned( - top: 14.w, - right: 52.w, - child: _buildHeaderIconButton( - iconAsset: - "sc_images/room/sc_icon_room_user_card_setting.png", - onTap: () { - if (userProvider?.userCardInfo == null) { - return; - } - Navigator.of(context).pop(); - showBottomInBottomDialog( - navigatorKey.currentState?.context ?? context, - RoomUserCardSetting( - roomId: roomId, - userCardInfo: userProvider?.userCardInfo?.copyWith(), - ), - ); - }, - ), + ), + if (canShowSetting) + Positioned( + top: 14.w, + right: 52.w, + child: _buildHeaderIconButton( + iconAsset: + "sc_images/room/sc_icon_room_user_card_setting.png", + onTap: () { + if (userProvider?.userCardInfo == null) { + return; + } + Navigator.of(context).pop(); + showBottomInBottomDialog( + navigatorKey.currentState?.context ?? context, + RoomUserCardSetting( + roomId: roomId, + userCardInfo: userProvider?.userCardInfo?.copyWith(), + ), + ); + }, ), - ], - ), + ), + ], ), ), ); @@ -262,6 +263,57 @@ class _RoomUserInfoCardState extends State { ); } + Widget _buildSheetBackground(PropsResources? dataCard) { + if (dataCard == null) { + return DecoratedBox(decoration: _sheetDecoration()); + } + return Stack( + fit: StackFit.expand, + children: [ + SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.08), + Colors.white.withValues(alpha: 0.22), + Colors.white.withValues(alpha: 0.58), + ], + ), + ), + ), + ], + ); + } + + PropsResources? _activeDataCard(room_card.RoomUserCardRes? cardInfo) { + return _activeDataCardFromUseProps(cardInfo?.userProfile?.useProps) ?? + _activeDataCardFromUseProps(cardInfo?.useProps); + } + + PropsResources? _activeDataCardFromUseProps(List? useProps) { + final now = DateTime.now().millisecondsSinceEpoch; + for (final item in useProps ?? const []) { + final resource = item.propsResources; + if (resource?.type != SCPropsType.DATA_CARD.name) { + continue; + } + final expireText = item.expireTime?.trim() ?? ""; + final expireTime = int.tryParse(expireText); + if (expireText.isNotEmpty && (expireTime ?? 0) <= now) { + continue; + } + final source = resource?.sourceUrl?.trim() ?? ""; + final cover = resource?.cover?.trim() ?? ""; + if (source.isNotEmpty || cover.isNotEmpty) { + return resource; + } + } + return null; + } + Widget _buildProfileHeader( BuildContext context, SocialChatUserProfile? profile, @@ -290,7 +342,7 @@ class _RoomUserInfoCardState extends State { ), SizedBox(width: 4.w), socialchatNickNameText( - maxWidth: 190.w, + maxWidth: 116.w, profile?.userNickname ?? "", fontSize: 18.sp, textColor: Colors.black, @@ -298,6 +350,30 @@ class _RoomUserInfoCardState extends State { type: "", needScroll: (profile?.userNickname?.characters.length ?? 0) > 12, ), + SizedBox(width: 3.w), + getVIPBadge( + profile?.vipLevel ?? profile?.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + SizedBox(width: 3.w), + getWealthLevel( + profile?.wealthLevel ?? + ref.userCardInfo?.userLevel?.wealthLevel ?? + 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + SizedBox(width: 2.w), + getUserLevel( + profile?.charmLevel ?? + ref.userCardInfo?.userLevel?.charmLevel ?? + 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), ], ), SizedBox(height: 2.w), @@ -422,10 +498,10 @@ class _RoomUserInfoCardState extends State { return SCUserBadgeStrip( userId: userId, fallbackBadges: _roomCardFallbackBadges(ref, profile), - height: 55.w, - badgeHeight: 23.w, - longBadgeWidth: 58.w, - spacing: 8.w, + height: 82.w, + badgeHeight: 34.5.w, + longBadgeWidth: 87.w, + spacing: 10.w, maxBadges: 12, wrap: true, reserveSpace: true, diff --git a/lib/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart b/lib/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart new file mode 100644 index 0000000..913866f --- /dev/null +++ b/lib/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/main.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/store_list_res.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_currency_type.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_tts.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/props/sc_data_card_resource_view.dart'; + +class PropsStoreDataCardDetailDialog extends StatefulWidget { + const PropsStoreDataCardDetailDialog(this.res, this.disCount, {super.key}); + + final StoreListRes res; + final double disCount; + + @override + State createState() => + _PropsStoreDataCardDetailDialogState(); +} + +class _PropsStoreDataCardDetailDialogState + extends State { + PropsPrices? currentPropsPrice; + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentPropsPrice = + (widget.res.propsPrices?.isNotEmpty ?? false) + ? widget.res.propsPrices![currentIndex] + : null; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + height: 360.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: const Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Column( + children: [ + SizedBox(height: 18.w), + ClipRRect( + borderRadius: BorderRadius.circular(12.w), + child: SizedBox( + width: 220.w, + height: 145.w, + child: SCDataCardResourceView( + resource: widget.res.propsResources, + fit: BoxFit.cover, + ), + ), + ), + SizedBox(height: 18.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 15.w), + Expanded( + child: text( + widget.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + maxLines: 1, + ), + ), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 22.w, + height: 22.w, + ), + if (widget.disCount < 1) + text( + "${currentPropsPrice?.amount ?? 0}", + textColor: SocialChatTheme.primaryLight, + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + if (widget.disCount < 1) SizedBox(width: 5.w), + text( + "${((currentPropsPrice?.amount ?? 0) * widget.disCount).toInt()}", + textColor: SocialChatTheme.primaryLight, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 10.w), + SizedBox( + height: 28.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: widget.res.propsPrices?.length ?? 0, + itemBuilder: (context, index) { + return GestureDetector( + child: Container( + width: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: const Color(0xff18F2B1).withValues(alpha: 0.1), + border: Border.all( + color: + index == currentIndex + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: text( + "${widget.res.propsPrices?[index].days} ${SCAppLocalizations.of(context)!.day}", + textColor: SocialChatTheme.primaryLight, + ), + ), + onTap: () { + currentIndex = index; + currentPropsPrice = widget.res.propsPrices?[index]; + setState(() {}); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + SizedBox(height: 18.w), + Container( + color: Colors.black12, + width: ScreenUtil().screenWidth, + height: 0.5.w, + ), + SizedBox(height: 18.w), + Row( + children: [ + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 20.w, + height: 20.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 13.sp, + textColor: SocialChatTheme.primaryLight, + ), + Icon( + Icons.chevron_right, + size: 20.w, + color: SocialChatTheme.primaryLight, + ), + ], + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + const Spacer(), + SCDebounceWidget( + onTap: _purchase, + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + SCAppLocalizations.of(context)!.purchase, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ), + ); + } + + void _purchase() { + if (currentPropsPrice == null) { + return; + } + final profileManager = Provider.of( + context, + listen: false, + ); + final localizations = SCAppLocalizations.of(context)!; + SCLoadingManager.show(); + SCStoreRepositoryImp() + .storePurchasing( + widget.res.id ?? "", + SCPropsType.DATA_CARD.name, + SCCurrencyType.GOLD.name, + "${currentPropsPrice?.days}", + ) + .then((value) { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCLoadingManager.hide(); + SCTts.show(localizations.purchaseIsSuccessful); + profileManager.updateBalance(value); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt index 93a3928..f9e089a 100644 --- a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt @@ -258,13 +258,25 @@ class VapView( result.success(null) } - "setScaleType" -> { - val type = call.argument("scaleType") - setScaleType(type) - result.success(null) - } - - "setVapTagContent" -> { + "setScaleType" -> { + val type = call.argument("scaleType") + setScaleType(type) + result.success(null) + } + + "enableVersion1" -> { + val enable = call.argument("enable") ?: false + animView.enableVersion1(enable) + result.success(null) + } + + "setVideoMode" -> { + val mode = call.argument("mode") ?: 1 + animView.setVideoMode(mode) + result.success(null) + } + + "setVapTagContent" -> { val tag = call.argument("tag") val contentMap = call.argument>("content") if (tag != null && contentMap != null) { diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift index 226c71d..9cbd53e 100644 --- a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift +++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift @@ -67,6 +67,8 @@ public class VapFlutterView: NSObject, FlutterPlatformView { scaleType = value } result(nil) + case "enableVersion1", "setVideoMode": + result(nil) case "setVapTagContent": if let args = call.arguments as? [String: Any], let tag = args["tag"] as? String, diff --git a/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart b/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart index 41afc68..65ec62c 100644 --- a/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart +++ b/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart @@ -396,6 +396,23 @@ class VapController { await __channel.invokeMethod('setScaleType', {'scaleType': scaleType}); } + /// Enables compatibility mode for legacy RGB/alpha split MP4 resources + /// that do not contain a VAP config box. + Future enableVersion1(bool enable) async { + await __channel.invokeMethod('enableVersion1', {'enable': enable}); + } + + /// Sets legacy split MP4 mode. + /// + /// Tencent VAP values: + /// - 1: alpha left, RGB right + /// - 2: alpha top, RGB bottom + /// - 3: RGB left, alpha right + /// - 4: RGB top, alpha bottom + Future setVideoMode(int mode) async { + await __channel.invokeMethod('setVideoMode', {'mode': mode}); + } + /// Sets content for a specific VAP tag. /// /// VAP tags are placeholders in the animation file that can be replaced diff --git a/test/sc_user_badge_res_test.dart b/test/sc_user_badge_res_test.dart new file mode 100644 index 0000000..59b40b5 --- /dev/null +++ b/test/sc_user_badge_res_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart'; + +void main() { + group('SCUserBadgeRes honor wall badges', () { + test('uses short badges only and sorts newest first', () { + final result = SCUserBadgeRes.fromJson({ + 'longBadges': [ + { + 'id': 'vip', + 'type': 'LONG', + 'sourceType': 'VIP', + 'selectUrl': 'https://example.com/vip.png', + 'updateTime': 3000, + }, + ], + 'shortBadges': [ + { + 'id': 'old', + 'type': 'SHORT', + 'sourceType': 'ACTIVITY', + 'selectUrl': 'https://example.com/old.png', + 'updateTime': 1000, + }, + { + 'id': 'new', + 'type': 'SHORT', + 'sourceType': 'ACHIEVEMENT', + 'selectUrl': 'https://example.com/new.png', + 'updateTime': 2000, + }, + ], + }); + + expect(result.displayBadges.map((badge) => badge.id), [ + 'vip', + 'old', + 'new', + ]); + expect(result.honorWallBadges.map((badge) => badge.id), ['new', 'old']); + }); + + test('filters long and vip badges from legacy mixed badges', () { + final result = SCUserBadgeRes.fromJson({ + 'badges': [ + { + 'id': 'long', + 'type': 'LONG', + 'selectUrl': 'https://example.com/long.png', + 'updateTime': 3000, + }, + { + 'id': 'vip', + 'type': 'VIP', + 'sourceType': 'VIP', + 'selectUrl': 'https://example.com/vip.png', + 'updateTime': 2500, + }, + { + 'id': 'activity', + 'type': 'ACTIVITY', + 'sourceType': 'ACTIVITY', + 'selectUrl': 'https://example.com/activity.png', + 'updateTime': 1000, + }, + { + 'id': 'achievement', + 'type': 'ACHIEVEMENT', + 'sourceType': 'ACHIEVEMENT', + 'selectUrl': 'https://example.com/achievement.png', + 'updateTime': 2000, + }, + ], + }); + + expect(result.honorWallBadges.map((badge) => badge.id), [ + 'achievement', + 'activity', + ]); + }); + }); +}