diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index eb7bee2..4f23570 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -389,7 +389,8 @@ "camera": "كاميرا", "system": "نظام", "selectYourCountry": "اختر دولتك", - "inviteCode": "رمز الدعوة", + "enterInvitationCode": "أدخل رمز الدعوة", + "inviteCode": "رمز الدعوة", "yesterday": "أمس {1}", "monday": "الاثنين {1}", "tuesday": "الثلاثاء {1}", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index 6f83a46..3f0b00e 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -426,6 +426,7 @@ "deleteAccount": "অ্যাকাউন্ট মুছে ফেলুন", "becomeAgent": "এজেন্ট হন", "enterNickname": "উপনাম লিখুন", + "enterInvitationCode": "আমন্ত্রণ কোড লিখুন", "selectYourCountry": "আপনার দেশ নির্বাচন করুন", "inviteCode": "আমন্ত্রণ কোড", "magic": "জাদু", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index fa87ca5..fc3620f 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -397,6 +397,7 @@ "deleteAccount": "Delete Account", "becomeAgent": "Become an agent", "enterNickname": "Enter Nickname", + "enterInvitationCode": "enter Invitation code", "selectYourCountry": "Select your country", "inviteCode": "Invite Code", "magic": "Magic", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index 571afd6..e45f451 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -393,11 +393,12 @@ "userEditing": "Kullanıcı Düzenleme", "enterTheUserId": "Kullanıcı ID'sini Girin", "enterTheRoomId": "Oda ID'sini Girin", - "deleteAccount": "Hesabı Sil", - "becomeAgent": "Temsilci Ol", - "enterNickname": "Takma Ad Girin", - "selectYourCountry": "Ülkenizi Seçin", - "inviteCode": "Davet Kodu", + "deleteAccount": "Hesabı Sil", + "becomeAgent": "Temsilci Ol", + "enterNickname": "Takma Ad Girin", + "enterInvitationCode": "Davet kodunu girin", + "selectYourCountry": "Ülkenizi Seçin", + "inviteCode": "Davet Kodu", "magic": "Sihir", "luckGiftSpecialEffects": "Şanslı hediye animasyon efektleri", "theVideoSizeCannotExceed": "Video boyutu 50M'yi geçemez", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 223ca63..1df3b09 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = S9X2AJ2US9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -693,7 +693,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -731,7 +731,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index e8bdb9e..ae04de6 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -812,6 +812,12 @@ class SCAppLocalizations { String get enterNickname => translate('enterNickname'); + String get enterInvitationCode { + final value = translate('enterInvitationCode'); + // Fallback to a readable English hint until every locale ships this key. + return value == 'enterInvitationCode' ? 'enter Invitation code' : value; + } + String get inviteCode => translate('inviteCode'); String get notifcation => translate('notifcation'); diff --git a/lib/main.dart b/lib/main.dart index 77a977a..3b0e05a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -285,13 +285,15 @@ class YumiApplication extends StatefulWidget { State createState() => _YumiApplicationState(); } -class _YumiApplicationState extends State { +class _YumiApplicationState extends State + with WidgetsBindingObserver { late fluro.FluroRouter _router; final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler(); @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); // 将非首帧必需的初始化统一延后到首帧后 WidgetsBinding.instance.addPostFrameCallback((_) { _initRouter(); @@ -300,11 +302,22 @@ class _YumiApplicationState extends State { } @override - dispose() { + void dispose() { + WidgetsBinding.instance.removeObserver(this); _deepLinkHandler.dispose(); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.detached) { + unawaited( + context.read().releaseRtcEngineForAppTermination(), + ); + } + } + Future _initLink() async { // 初始化,并传递一个回调函数用于处理链接 await _deepLinkHandler.initDeepLinks( diff --git a/lib/modules/auth/edit/sc_edit_profile_page.dart b/lib/modules/auth/edit/sc_edit_profile_page.dart index 6aa5ae9..485f18d 100644 --- a/lib/modules/auth/edit/sc_edit_profile_page.dart +++ b/lib/modules/auth/edit/sc_edit_profile_page.dart @@ -1,645 +1,678 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/shared/tools/sc_loading_manager.dart'; -import 'package:yumi/shared/tools/sc_pick_utils.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/services/auth/authentication_manager.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/app/routes/sc_routes.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_date_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/services/general/sc_app_general_manager.dart'; -import 'package:yumi/services/auth/user_profile_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; - -import '../../../shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart'; -import '../../country/country_route.dart'; - -///编辑个人信息 -class SCEditProfilePage extends StatefulWidget { - const SCEditProfilePage({super.key}); - - @override - _SCEditProfilePageState createState() => _SCEditProfilePageState(); -} - -class _SCEditProfilePageState extends State { - ///默认女 - int type = 0; - DateTime birthdayDate = DateTime(2006); - TextEditingController nicknameController = TextEditingController(); - SocialChatUserProfileManager? userProvider; - - @override - void initState() { - super.initState(); - userProvider = Provider.of( - context, - listen: false, - ); - Provider.of(context, listen: false).selectCountryInfo = - null; - } - - @override - void dispose() { - userProvider?.resetEditUserData(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; - return Scaffold( - resizeToAvoidBottomInset: false, - body: Stack( - alignment: Alignment.topCenter, - children: [ - Image.asset( - businessLogicStrategy.getEditProfileBackgroundImage(), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.cover, - ), - Column( - children: [ - SizedBox(height: 120.w), - GestureDetector( - child: Consumer( - builder: (_, provider, __) { - return provider.editUser != null && - provider.editUser!.userAvatar != null - ? head( - url: provider.editUser!.userAvatar ?? "", - width: 75.w, - ) - : Image.asset( - businessLogicStrategy - .getEditProfileDefaultAvatarImage(), - width: 75.w, - height: 75.w, - ); - }, - ), - onTap: () { - SCPickUtils.pickImage(context, (bool success, String url) { - if (success) { - userProvider?.updateUserAvatar(url); - } - }); - }, - ), - SizedBox(height: 15.w), - Container( - padding: EdgeInsets.only(left: width(12), right: width(12)), - alignment: Alignment.center, - height: 46.w, - width: 300.w, - decoration: BoxDecoration( - color: - businessLogicStrategy - .getEditProfileInputBackgroundColor(), - borderRadius: BorderRadius.all(Radius.circular(height(8))), - ), - child: TextField( - textAlign: TextAlign.center, - controller: nicknameController, - onChanged: (text) { - setState(() {}); - }, - inputFormatters: [SCCustomFilteringTextInputFormatter()], - maxLength: 38, - decoration: InputDecoration( - hintText: SCAppLocalizations.of(context)!.enterNickname, - hintStyle: TextStyle(color: Colors.white, fontSize: 14.sp), - contentPadding: EdgeInsets.only(top: 0.w), - counterText: '', - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: Colors.black45, - ), - style: TextStyle( - fontSize: sp(15), - color: Colors.white, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - SizedBox(height: 15.w), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - child: Container( - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 2.w, - color: - type == 1 - ? Color(0xff18F2B1) - : Colors.transparent, - ), - ), - child: Image.asset( - businessLogicStrategy.getEditProfileGenderIcon( - true, - ), - width: 55.w, - height: 55.w, - ), - ), - SizedBox(height: 8.w), - text( - SCAppLocalizations.of(context)!.male, - textColor: type == 1 ? Colors.white : Colors.white, - fontWeight: - type == 1 ? FontWeight.w600 : FontWeight.w400, - fontSize: 13.sp, - ), - ], - ), - ), - onTap: () { - if (type == 0) { - setState(() { - type = 1; - userProvider?.updateUserSex(type); - }); - } - }, - ), - SizedBox(width: 99.w), - GestureDetector( - child: Container( - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 2.w, - color: - type == 1 - ? Colors.transparent - : Color(0xff18F2B1), - ), - ), - child: Image.asset( - businessLogicStrategy.getEditProfileGenderIcon( - false, - ), - width: 55.w, - height: 55.w, - ), - ), - SizedBox(height: 8.w), - text( - SCAppLocalizations.of(context)!.female, - textColor: type == 1 ? Colors.white : Colors.white, - fontWeight: - type == 1 ? FontWeight.w400 : FontWeight.w600, - fontSize: 13.sp, - ), - ], - ), - ), - onTap: () { - if (type == 1) { - setState(() { - type = 0; - userProvider?.updateUserSex(type); - }); - } - }, - ), - ], - ), - SizedBox(height: 12.w), - GestureDetector( - child: Container( - padding: EdgeInsets.only(left: width(12), right: width(12)), - alignment: Alignment.center, - height: 46.w, - width: 300.w, - decoration: BoxDecoration( - color: - businessLogicStrategy - .getEditProfileInputBackgroundColor(), - borderRadius: BorderRadius.all(Radius.circular(height(12))), - ), - child: Row( - children: [ - text( - SCAppLocalizations.of(context)!.birthday, - fontSize: 15.sp, - textColor: Colors.white, - ), - Spacer(), - text( - SCMDateUtils.formatDateTime(birthdayDate), - textColor: Colors.white54, - fontSize: 15.sp, - ), - Icon( - Icons.chevron_right_outlined, - color: Colors.white54, - size: 20.w, - ), - ], - ), - ), - onTap: () { - _selectDate(); - }, - ), - SizedBox(height: 12.w), - GestureDetector( - child: Consumer( - builder: (_, provider, __) { - return Container( - padding: EdgeInsets.only( - left: width(12), - right: width(12), - ), - alignment: Alignment.center, - height: 46.w, - width: 300.w, - decoration: BoxDecoration( - color: - businessLogicStrategy - .getEditProfileInputBackgroundColor(), - borderRadius: BorderRadius.all( - Radius.circular(height(12)), - ), - ), - child: - provider.selectCountryInfo != null - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - text( - SCAppLocalizations.of( - context, - )!.countryRegion, - fontSize: 15.sp, - textColor: Colors.white, - ), - Spacer(), - - netImage( - url: - provider - .selectCountryInfo! - .nationalFlag ?? - "", - borderRadius: BorderRadius.circular(3), - width: 26.w, - height: 16.w, - ), - SizedBox(width: 3.w), - text( - provider.selectCountryInfo!.aliasName ?? "", - textColor: Colors.white54, - fontSize: 14.sp, - ), - Icon( - Icons.chevron_right_outlined, - color: Colors.white54, - size: 20.w, - ), - ], - ) - : Row( - children: [ - text( - SCAppLocalizations.of( - context, - )!.countryRegion, - fontSize: 15.sp, - textColor: Colors.white, - ), - Spacer(), - text( - SCAppLocalizations.of( - context, - )!.selectYourCountry, - textColor: Colors.white54, - fontSize: 15.sp, - ), - Icon( - Icons.chevron_right_outlined, - color: Colors.white54, - size: 20.w, - ), - ], - ), - ); - }, - ), - onTap: () { - SCNavigatorUtils.push( - context, - CountryRoute.country, - replace: false, - ); - }, - ), - SizedBox(height: 45.w), - GestureDetector( - child: Container( - padding: EdgeInsets.only(left: width(12), right: width(12)), - alignment: Alignment.center, - height: 46.w, - width: 270.w, - decoration: BoxDecoration( - color: - businessLogicStrategy - .getEditProfileContinueButtonColor(), - borderRadius: BorderRadius.all(Radius.circular(height(12))), - ), - child: text( - SCAppLocalizations.of(context)!.conntinue, - textColor: Colors.white, - fontSize: 15.sp, - ), - ), - onTap: () { - _submitUserProfile(); - }, - ), - ], - ), - ], - ), - ); - } - - Future _selectDate() async { - showCupertinoModalPopup( - context: context, - builder: - (context) => SafeArea( - top: false, - child: Container( - height: 240.w, - padding: EdgeInsets.only(top: 6), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/login/sc_icon_login_edit_data_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: Column( - children: [ - SizedBox(height: 10.w), - Row( - children: [ - SizedBox(width: 10.w), - GestureDetector( - child: Container( - alignment: Alignment.topCenter, - child: text( - SCAppLocalizations.of(context)!.cancel, - textColor: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.w600, - ), - ), - onTap: () { - Navigator.of(context).pop(); - }, - ), - Spacer(), - GestureDetector( - child: Container( - alignment: Alignment.topCenter, - child: text( - SCAppLocalizations.of(context)!.confirm, - textColor: - SCGlobalConfig.businessLogicStrategy - .getEditProfileDatePickerConfirmColor(), - fontSize: 15.sp, - fontWeight: FontWeight.w600, - ), - ), - onTap: () { - setState(() {}); - Navigator.of(context).pop(); - }, - ), - SizedBox(width: 10.w), - ], - ), - Expanded( - child: CupertinoTheme( - data: CupertinoThemeData( - textTheme: CupertinoTextThemeData( - // 在这里设置字体颜色、大小等样式 - dateTimePickerTextStyle: TextStyle( - color: Colors.white, // 设置字体颜色 - fontSize: 16.sp, // 设置字体大小 - ), - ), - ), - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, - initialDateTime: _getBefor18(), - maximumDate: _getBefor18(), - onDateTimeChanged: (date) => birthdayDate = date, - ), - ), - ), - ], - ), - ), - ), - ); - } - - DateTime _getBefor18() { - DateTime currentDate = DateTime.now(); - DateTime eighteenYearsAgo = DateTime( - currentDate.year - 18, - currentDate.month, - currentDate.day, - currentDate.hour, - currentDate.minute, - currentDate.second, - currentDate.millisecond, - currentDate.microsecond, - ); - return eighteenYearsAgo; - } - - String? _preferNonEmpty(String? primary, String? fallback) { - if (primary != null && primary.isNotEmpty) { - return primary; - } - if (fallback != null && fallback.isNotEmpty) { - return fallback; - } - return primary ?? fallback; - } - - bool _isBrokenLocalMediaUrl(String? url) { - return (url ?? "").contains("/external/oss/local/"); - } - - String? _preferUsableAvatar(String? primary, String? fallback) { - if (primary != null && - primary.isNotEmpty && - !_isBrokenLocalMediaUrl(primary)) { - return primary; - } - if (fallback != null && - fallback.isNotEmpty && - !_isBrokenLocalMediaUrl(fallback)) { - return fallback; - } - return _preferNonEmpty(primary, fallback); - } - - num? _preferNonZero(num? primary, num? fallback) { - if (primary != null && primary != 0) { - return primary; - } - if (fallback != null && fallback != 0) { - return fallback; - } - return primary ?? fallback; - } - - ///提交数据 - void _submitUserProfile() async { - if (nicknameController.text.isEmpty) { - SCTts.show(SCAppLocalizations.of(context)!.pleaseEnterNickname); - return; - } - if (userProvider?.editUser?.userAvatar == null) { - SCTts.show(SCAppLocalizations.of(context)!.pleaseUploadUserAvatar); - return; - } - if (Provider.of( - context, - listen: false, - ).selectCountryInfo == - null) { - SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectYourCountry); - return; - } - - SCLoadingManager.show(context: context); - userProvider?.updateUserNickname(nicknameController.text); - userProvider?.updateBornYear(birthdayDate.year); - userProvider?.updateBornMonth(birthdayDate.month); - userProvider?.updateBornDay(birthdayDate.day); - num age = DateTime.now().year - birthdayDate.year; - userProvider?.updateAge(age.abs()); - String authType = - Provider.of( - context, - listen: false, - ).authType; - String idToken = - Provider.of( - context, - listen: false, - ).uid; - SocialChatLoginRes user = await SCAccountRepository().regist( - authType, - idToken, - userProvider!.editUser!, - ); - final submittedProfile = userProvider?.editUser; - if (submittedProfile != null) { - final mergedProfile = (user.userProfile ?? SocialChatUserProfile()) - .copyWith( - userAvatar: _preferUsableAvatar( - user.userProfile?.userAvatar, - submittedProfile.userAvatar, - ), - userNickname: _preferNonEmpty( - user.userProfile?.userNickname, - submittedProfile.userNickname, - ), - userSex: _preferNonZero( - user.userProfile?.userSex, - submittedProfile.userSex, - ), - bornYear: _preferNonZero( - user.userProfile?.bornYear, - submittedProfile.bornYear, - ), - bornMonth: _preferNonZero( - user.userProfile?.bornMonth, - submittedProfile.bornMonth, - ), - bornDay: _preferNonZero( - user.userProfile?.bornDay, - submittedProfile.bornDay, - ), - age: _preferNonZero(user.userProfile?.age, submittedProfile.age), - countryCode: _preferNonEmpty( - user.userProfile?.countryCode, - submittedProfile.countryCode, - ), - countryId: _preferNonEmpty( - user.userProfile?.countryId, - submittedProfile.countryId, - ), - countryName: _preferNonEmpty( - user.userProfile?.countryName, - submittedProfile.countryName, - ), - ); - user.setUserProfile(mergedProfile); - } - if (!mounted) { - return; - } - await Provider.of(context, listen: false).resetLocalRoomState( - fallbackRtmProvider: Provider.of(context, listen: false), - ); - if (!mounted) { - return; - } - AccountStorage().setCurrentUser(user); - userProvider?.syncCurrentUserProfile(user.userProfile); - await DataPersistence.clearPendingRegisterRewardDialog(); - await DataPersistence.setAwaitRegisterRewardSocket(true); - if (!mounted) { - return; - } - SCLoadingManager.hide(); - SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); - } -} +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/auth/authentication_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +import '../../../shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart'; +import '../../country/country_route.dart'; + +///编辑个人信息 +class SCEditProfilePage extends StatefulWidget { + const SCEditProfilePage({super.key}); + + @override + State createState() => _SCEditProfilePageState(); +} + +class _SCEditProfilePageState extends State { + ///默认女 + int type = 0; + DateTime birthdayDate = DateTime(2006); + final TextEditingController nicknameController = TextEditingController(); + final TextEditingController invitationCodeController = + TextEditingController(); + SocialChatUserProfileManager? userProvider; + + @override + void initState() { + super.initState(); + userProvider = Provider.of( + context, + listen: false, + ); + Provider.of(context, listen: false).selectCountryInfo = + null; + } + + @override + void dispose() { + nicknameController.dispose(); + invitationCodeController.dispose(); + userProvider?.resetEditUserData(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; + return Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.topCenter, + children: [ + Image.asset( + businessLogicStrategy.getEditProfileBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Column( + children: [ + SizedBox(height: 120.w), + GestureDetector( + child: Consumer( + builder: (_, provider, __) { + return provider.editUser != null && + provider.editUser!.userAvatar != null + ? head( + url: provider.editUser!.userAvatar ?? "", + width: 75.w, + ) + : Image.asset( + businessLogicStrategy + .getEditProfileDefaultAvatarImage(), + width: 75.w, + height: 75.w, + ); + }, + ), + onTap: () { + SCPickUtils.pickImage(context, (bool success, String url) { + if (success) { + userProvider?.updateUserAvatar(url); + } + }); + }, + ), + SizedBox(height: 15.w), + _buildInputField( + controller: nicknameController, + hintText: SCAppLocalizations.of(context)!.enterNickname, + inputFormatters: [SCCustomFilteringTextInputFormatter()], + onChanged: (text) { + setState(() {}); + }, + ), + SizedBox(height: 15.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2.w, + color: + type == 1 + ? Color(0xff18F2B1) + : Colors.transparent, + ), + ), + child: Image.asset( + businessLogicStrategy.getEditProfileGenderIcon( + true, + ), + width: 55.w, + height: 55.w, + ), + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.male, + textColor: type == 1 ? Colors.white : Colors.white, + fontWeight: + type == 1 ? FontWeight.w600 : FontWeight.w400, + fontSize: 13.sp, + ), + ], + ), + ), + onTap: () { + if (type == 0) { + setState(() { + type = 1; + userProvider?.updateUserSex(type); + }); + } + }, + ), + SizedBox(width: 99.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2.w, + color: + type == 1 + ? Colors.transparent + : Color(0xff18F2B1), + ), + ), + child: Image.asset( + businessLogicStrategy.getEditProfileGenderIcon( + false, + ), + width: 55.w, + height: 55.w, + ), + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.female, + textColor: type == 1 ? Colors.white : Colors.white, + fontWeight: + type == 1 ? FontWeight.w400 : FontWeight.w600, + fontSize: 13.sp, + ), + ], + ), + ), + onTap: () { + if (type == 1) { + setState(() { + type = 0; + userProvider?.updateUserSex(type); + }); + } + }, + ), + ], + ), + SizedBox(height: 12.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(12))), + ), + child: Row( + children: [ + text( + SCAppLocalizations.of(context)!.birthday, + fontSize: 15.sp, + textColor: Colors.white, + ), + Spacer(), + text( + SCMDateUtils.formatDateTime(birthdayDate), + textColor: Colors.white54, + fontSize: 15.sp, + ), + Icon( + Icons.chevron_right_outlined, + color: Colors.white54, + size: 20.w, + ), + ], + ), + ), + onTap: () { + _selectDate(); + }, + ), + SizedBox(height: 12.w), + GestureDetector( + child: Consumer( + builder: (_, provider, __) { + final country = provider.selectCountryInfo; + return _buildSelectorField( + label: SCAppLocalizations.of(context)!.countryRegion, + value: + country?.aliasName ?? + SCAppLocalizations.of(context)!.selectYourCountry, + valueFontSize: country != null ? 14.sp : 15.sp, + leading: + country == null + ? null + : netImage( + url: country.nationalFlag ?? "", + borderRadius: BorderRadius.circular(3), + width: 26.w, + height: 16.w, + ), + ); + }, + ), + onTap: () { + SCNavigatorUtils.push( + context, + CountryRoute.country, + replace: false, + ); + }, + ), + SizedBox(height: 12.w), + _buildInputField( + controller: invitationCodeController, + hintText: SCAppLocalizations.of(context)!.enterInvitationCode, + inputFormatters: [ + SCCustomFilteringTextInputFormatter(), + LengthLimitingTextInputFormatter(10), + ], + maxLength: 10, + ), + SizedBox(height: 45.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 270.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileContinueButtonColor(), + borderRadius: BorderRadius.all(Radius.circular(height(12))), + ), + child: text( + SCAppLocalizations.of(context)!.conntinue, + textColor: Colors.white, + fontSize: 15.sp, + ), + ), + onTap: () { + _submitUserProfile(); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildInputField({ + required TextEditingController controller, + required String hintText, + List? inputFormatters, + ValueChanged? onChanged, + int maxLength = 38, + }) { + return Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(8))), + ), + child: TextField( + textAlign: TextAlign.center, + controller: controller, + onChanged: onChanged, + inputFormatters: inputFormatters, + maxLength: maxLength, + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle(color: Colors.white, fontSize: 14.sp), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Colors.black45, + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ); + } + + Widget _buildSelectorField({ + required String label, + required String value, + Widget? leading, + double? valueFontSize, + }) { + return Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(12))), + ), + child: Row( + children: [ + Flexible( + flex: 4, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Colors.white, fontSize: 15.sp), + ), + ), + SizedBox(width: 8.w), + Expanded( + flex: 5, + child: Row( + children: [ + if (leading != null) ...[leading, SizedBox(width: 6.w)], + Expanded( + child: Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white54, + fontSize: valueFontSize ?? 15.sp, + ), + ), + ), + SizedBox(width: 4.w), + Icon( + Icons.chevron_right_outlined, + color: Colors.white54, + size: 20.w, + ), + ], + ), + ), + ], + ), + ); + } + + Future _selectDate() async { + showCupertinoModalPopup( + context: context, + builder: + (context) => SafeArea( + top: false, + child: Container( + height: 240.w, + padding: EdgeInsets.only(top: 6), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/login/sc_icon_login_edit_data_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.cancel, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Navigator.of(context).pop(); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.confirm, + textColor: + SCGlobalConfig.businessLogicStrategy + .getEditProfileDatePickerConfirmColor(), + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + setState(() {}); + Navigator.of(context).pop(); + }, + ), + SizedBox(width: 10.w), + ], + ), + Expanded( + child: CupertinoTheme( + data: CupertinoThemeData( + textTheme: CupertinoTextThemeData( + // 在这里设置字体颜色、大小等样式 + dateTimePickerTextStyle: TextStyle( + color: Colors.white, // 设置字体颜色 + fontSize: 16.sp, // 设置字体大小 + ), + ), + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: _getBefor18(), + maximumDate: _getBefor18(), + onDateTimeChanged: (date) => birthdayDate = date, + ), + ), + ), + ], + ), + ), + ), + ); + } + + DateTime _getBefor18() { + DateTime currentDate = DateTime.now(); + DateTime eighteenYearsAgo = DateTime( + currentDate.year - 18, + currentDate.month, + currentDate.day, + currentDate.hour, + currentDate.minute, + currentDate.second, + currentDate.millisecond, + currentDate.microsecond, + ); + return eighteenYearsAgo; + } + + String? _preferNonEmpty(String? primary, String? fallback) { + if (primary != null && primary.isNotEmpty) { + return primary; + } + if (fallback != null && fallback.isNotEmpty) { + return fallback; + } + return primary ?? fallback; + } + + bool _isBrokenLocalMediaUrl(String? url) { + return (url ?? "").contains("/external/oss/local/"); + } + + String? _preferUsableAvatar(String? primary, String? fallback) { + if (primary != null && + primary.isNotEmpty && + !_isBrokenLocalMediaUrl(primary)) { + return primary; + } + if (fallback != null && + fallback.isNotEmpty && + !_isBrokenLocalMediaUrl(fallback)) { + return fallback; + } + return _preferNonEmpty(primary, fallback); + } + + num? _preferNonZero(num? primary, num? fallback) { + if (primary != null && primary != 0) { + return primary; + } + if (fallback != null && fallback != 0) { + return fallback; + } + return primary ?? fallback; + } + + ///提交数据 + void _submitUserProfile() async { + FocusScope.of(context).unfocus(); + if (nicknameController.text.isEmpty) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseEnterNickname); + return; + } + if (userProvider?.editUser?.userAvatar == null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseUploadUserAvatar); + return; + } + if (Provider.of( + context, + listen: false, + ).selectCountryInfo == + null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectYourCountry); + return; + } + + SCLoadingManager.show(context: context); + userProvider?.updateUserNickname(nicknameController.text); + userProvider?.updateBornYear(birthdayDate.year); + userProvider?.updateBornMonth(birthdayDate.month); + userProvider?.updateBornDay(birthdayDate.day); + num age = DateTime.now().year - birthdayDate.year; + userProvider?.updateAge(age.abs()); + String authType = + Provider.of( + context, + listen: false, + ).authType; + String idToken = + Provider.of( + context, + listen: false, + ).uid; + // TODO(register): When the backend registration API supports invitation + // codes, pass `invitationCodeController.text.trim()` here as an optional + // argument. Empty string should remain allowed. + SocialChatLoginRes user = await SCAccountRepository().regist( + authType, + idToken, + userProvider!.editUser!, + ); + final submittedProfile = userProvider?.editUser; + if (submittedProfile != null) { + final mergedProfile = (user.userProfile ?? SocialChatUserProfile()) + .copyWith( + userAvatar: _preferUsableAvatar( + user.userProfile?.userAvatar, + submittedProfile.userAvatar, + ), + userNickname: _preferNonEmpty( + user.userProfile?.userNickname, + submittedProfile.userNickname, + ), + userSex: _preferNonZero( + user.userProfile?.userSex, + submittedProfile.userSex, + ), + bornYear: _preferNonZero( + user.userProfile?.bornYear, + submittedProfile.bornYear, + ), + bornMonth: _preferNonZero( + user.userProfile?.bornMonth, + submittedProfile.bornMonth, + ), + bornDay: _preferNonZero( + user.userProfile?.bornDay, + submittedProfile.bornDay, + ), + age: _preferNonZero(user.userProfile?.age, submittedProfile.age), + countryCode: _preferNonEmpty( + user.userProfile?.countryCode, + submittedProfile.countryCode, + ), + countryId: _preferNonEmpty( + user.userProfile?.countryId, + submittedProfile.countryId, + ), + countryName: _preferNonEmpty( + user.userProfile?.countryName, + submittedProfile.countryName, + ), + ); + user.setUserProfile(mergedProfile); + } + if (!mounted) { + return; + } + await Provider.of(context, listen: false).resetLocalRoomState( + fallbackRtmProvider: Provider.of(context, listen: false), + ); + if (!mounted) { + return; + } + AccountStorage().setCurrentUser(user); + userProvider?.syncCurrentUserProfile(user.userProfile); + await DataPersistence.clearPendingRegisterRewardDialog(); + await DataPersistence.setAwaitRegisterRewardSocket(true); + if (!mounted) { + return; + } + SCLoadingManager.hide(); + SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); + } +} diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index d6796b8..6857ddf 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -1,315 +1,315 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.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/modules/index/main_route.dart'; -import 'package:yumi/modules/store/store_route.dart'; -import 'package:yumi/modules/user/settings/settings_route.dart'; -import 'package:yumi/modules/wallet/wallet_route.dart'; -import 'package:yumi/services/auth/user_profile_manager.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.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/widgets/id/sc_special_id_badge.dart'; - -class MePage2 extends StatefulWidget { - const MePage2({super.key}); - - @override - State createState() => _MePage2State(); -} - -class _MePage2State extends State { - Map _counterMap = {}; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadPageData(); - }); - } - - void _loadPageData() { - final profileManager = Provider.of( - context, - listen: false, - ); - profileManager.fetchUserProfileData(); - profileManager.balance(); - _loadCounter(); - } - - Future _loadCounter() async { - final userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; - if (userId.isEmpty) return; - - try { - final userCounterList = await SCAccountRepository().userCounter(userId); - if (!mounted) return; - setState(() { - _counterMap = Map.fromEntries( - userCounterList.map( - (counter) => MapEntry(counter.counterType ?? "", counter), - ), - ); - }); - } catch (_) {} - } - - @override - Widget build(BuildContext context) { - return SafeArea( - top: true, - bottom: false, - child: Consumer( - builder: (context, profileManager, child) { - return SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w), - child: Column( - children: [ - _buildProfileSection(profileManager), - SizedBox(height: 12.w), - _buildStatsCard(), - SizedBox(height: 12.w), - _buildEntryRow(), - SizedBox(height: 12.w), - _buildWalletCard(), - SizedBox(height: 12.w), - _buildMenuCard2(), - SizedBox(height: 12.w), - _buildMenuCard([ - _MenuRowData( - title: SCAppLocalizations.of(context)!.settings, - assetIcon: 'sc_images/index/sc_icon_settings.png', - onTap: () { - SCNavigatorUtils.push( - context, - SettingsRoute.settings, - replace: false, - ); - }, - ), - _MenuRowData( - title: SCAppLocalizations.of(context)!.language, - assetIcon: 'sc_images/general/sc_icon_setting_language.png', - onTap: () { - SCNavigatorUtils.push(context, SCMainRoute.language); - }, - ), - ]), - ], - ), - ); - }, - ), - ); - } - - Widget _buildProfileSection(SocialChatUserProfileManager profileManager) { - final profile = profileManager.currentUserProfile; - - return SCDebounceWidget( - onTap: () { - SCNavigatorUtils.push( - context, - '${SCMainRoute.person}?isMe=true&tageId=${profile?.id ?? ""}', - replace: false, - ); - }, - child: Column( - children: [ - SizedBox( - width: 240.w, - height: 130.w, - child: Stack( - alignment: Alignment.topCenter, - children: [ - head( - url: profile?.userAvatar ?? '', - width: 96.w, - height: 96.w, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 1), - headdress: profile?.getHeaddress()?.sourceUrl, - showDefault: true, - ), - ], - ), - ), - SizedBox(height: 8.w), - Text( - profile?.userNickname ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 18.sp, - fontWeight: FontWeight.w700, - ), - ), - SizedBox(height: 2.w), - SCSpecialIdBadge( - idText: profile?.getID() ?? '', - showAnimated: profile?.hasSpecialId() ?? false, - assetPath: SCSpecialIdAssets.userIdLarge, - animationWidth: 80.w, - animationHeight: 32.w, - showTextBesideAnimated: true, - animatedTextSpacing: 0, - showAnimatedGradientText: - profile?.shouldShowColoredSpecialIdText() ?? false, - animationTextStyle: TextStyle( - color: Colors.white, - fontSize: 16.sp, - fontWeight: FontWeight.w700, - ), - normalTextStyle: TextStyle( - color: Colors.white, - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - animationFit: BoxFit.contain, - ), - ], - ), - ); - } - - Widget _buildStatsCard() { - return _buildGlassCard( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - radius: 8.w, - child: Row( - children: [ - _buildStatItem( - value: _counterText('INTERVIEW'), - label: SCAppLocalizations.of(context)!.vistors, - onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors), - ), - _buildStatDivider(), - _buildStatItem( - value: _counterText('SUBSCRIPTION'), - label: SCAppLocalizations.of(context)!.following, - onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow), - ), - _buildStatDivider(), - _buildStatItem( - value: _counterText('FANS'), - label: SCAppLocalizations.of(context)!.friends, - onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans), - ), - ], - ), - ); - } - - Widget _buildEntryRow() { - return Row( - children: [ - _buildEntryItem( - title: SCAppLocalizations.of(context)!.store, - iconPath: 'sc_images/index/sc_icon_shop.png', - onTap: () => SCNavigatorUtils.push(context, StoreRoute.list), - ), - SizedBox(width: 10.w), - _buildEntryItem( - title: SCAppLocalizations.of(context)!.bag, - iconPath: 'sc_images/index/sc_icon_bag.png', - onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags), - ), - SizedBox(width: 10.w), - _buildEntryItem( - title: SCAppLocalizations.of(context)!.level, - iconPath: 'sc_images/index/sc_icon_level.png', - onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList), - ), - ], - ); - } - - Widget _buildWalletCard() { - return SCDebounceWidget( - onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge), - child: Container( - width: double.infinity, - height: 90.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.w), - image: const DecorationImage( - image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'), - fit: BoxFit.fill, - ), - ), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - SCAppLocalizations.of(context)!.wallet, - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w800, - fontStyle: FontStyle.italic, - color: Colors.white, - ), - ), - SizedBox(height: 4.w), - Row( - children: [ - Image.asset( - 'sc_images/general/sc_icon_jb.png', - width: 28.w, - height: 28.w, - ), - SizedBox(width: 6.w), - Consumer( - builder: (context, ref, child) { - return Text( - ':${_balanceText(ref.myBalance)}', - style: TextStyle( - color: Colors.white, - fontSize: 24.sp, - fontWeight: FontWeight.w700, - fontStyle: FontStyle.italic, - ), - ); - }, - ), - ], - ), - ], - ), - ), - Image.asset( - 'sc_images/index/sc_icon_wallet_icon.png', - width: 68.w, - height: 68.w, - ), - ], - ), - ), - ); - } - - Widget _buildMenuCard2() { - final userProfile = context.watch(); - final items = <_MenuRowData>[]; - final hasCurrentUser = userProfile.currentUserProfile != null; - final isAgent = userProfile.userIdentity?.agent ?? false; - - // 1. 主播/代理入口 - if (hasCurrentUser) { - if (isAgent) { +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.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/modules/index/main_route.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.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/widgets/id/sc_special_id_badge.dart'; + +class MePage2 extends StatefulWidget { + const MePage2({super.key}); + + @override + State createState() => _MePage2State(); +} + +class _MePage2State extends State { + Map _counterMap = {}; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadPageData(); + }); + } + + void _loadPageData() { + final profileManager = Provider.of( + context, + listen: false, + ); + profileManager.fetchUserProfileData(); + profileManager.balance(); + _loadCounter(); + } + + Future _loadCounter() async { + final userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; + if (userId.isEmpty) return; + + try { + final userCounterList = await SCAccountRepository().userCounter(userId); + if (!mounted) return; + setState(() { + _counterMap = Map.fromEntries( + userCounterList.map( + (counter) => MapEntry(counter.counterType ?? "", counter), + ), + ); + }); + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: true, + bottom: false, + child: Consumer( + builder: (context, profileManager, child) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w), + child: Column( + children: [ + _buildProfileSection(profileManager), + SizedBox(height: 12.w), + _buildStatsCard(), + SizedBox(height: 12.w), + _buildEntryRow(), + SizedBox(height: 12.w), + _buildWalletCard(), + SizedBox(height: 12.w), + _buildMenuCard2(), + SizedBox(height: 12.w), + _buildMenuCard([ + _MenuRowData( + title: SCAppLocalizations.of(context)!.settings, + assetIcon: 'sc_images/index/sc_icon_settings.png', + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.settings, + replace: false, + ); + }, + ), + _MenuRowData( + title: SCAppLocalizations.of(context)!.language, + assetIcon: 'sc_images/general/sc_icon_setting_language.png', + onTap: () { + SCNavigatorUtils.push(context, SCMainRoute.language); + }, + ), + ]), + ], + ), + ); + }, + ), + ); + } + + Widget _buildProfileSection(SocialChatUserProfileManager profileManager) { + final profile = profileManager.currentUserProfile; + + return SCDebounceWidget( + onTap: () { + SCNavigatorUtils.push( + context, + '${SCMainRoute.person}?isMe=true&tageId=${profile?.id ?? ""}', + replace: false, + ); + }, + child: Column( + children: [ + SizedBox( + width: 240.w, + height: 130.w, + child: Stack( + alignment: Alignment.topCenter, + children: [ + head( + url: profile?.userAvatar ?? '', + width: 96.w, + height: 96.w, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1), + headdress: profile?.getHeaddress()?.sourceUrl, + showDefault: true, + ), + ], + ), + ), + SizedBox(height: 8.w), + Text( + profile?.userNickname ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 2.w), + SCSpecialIdBadge( + idText: profile?.getID() ?? '', + showAnimated: profile?.hasSpecialId() ?? false, + assetPath: SCSpecialIdAssets.userIdLarge, + animationWidth: 80.w, + animationHeight: 32.w, + showTextBesideAnimated: true, + animatedTextSpacing: 0, + showAnimatedGradientText: + profile?.shouldShowColoredSpecialIdText() ?? false, + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + normalTextStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + animationFit: BoxFit.contain, + ), + ], + ), + ); + } + + Widget _buildStatsCard() { + return _buildGlassCard( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + radius: 8.w, + child: Row( + children: [ + _buildStatItem( + value: _counterText('INTERVIEW'), + label: SCAppLocalizations.of(context)!.vistors, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors), + ), + _buildStatDivider(), + _buildStatItem( + value: _counterText('SUBSCRIPTION'), + label: SCAppLocalizations.of(context)!.following, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow), + ), + _buildStatDivider(), + _buildStatItem( + value: _counterText('FANS'), + label: SCAppLocalizations.of(context)!.friends, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans), + ), + ], + ), + ); + } + + Widget _buildEntryRow() { + return Row( + children: [ + _buildEntryItem( + title: SCAppLocalizations.of(context)!.store, + iconPath: 'sc_images/index/sc_icon_shop.png', + onTap: () => SCNavigatorUtils.push(context, StoreRoute.list), + ), + SizedBox(width: 10.w), + _buildEntryItem( + title: SCAppLocalizations.of(context)!.bag, + iconPath: 'sc_images/index/sc_icon_bag.png', + onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags), + ), + SizedBox(width: 10.w), + _buildEntryItem( + title: SCAppLocalizations.of(context)!.level, + iconPath: 'sc_images/index/sc_icon_level.png', + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList), + ), + ], + ); + } + + Widget _buildWalletCard() { + return SCDebounceWidget( + onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge), + child: Container( + width: double.infinity, + height: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + image: const DecorationImage( + image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'), + fit: BoxFit.fill, + ), + ), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + SCAppLocalizations.of(context)!.wallet, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + color: Colors.white, + ), + ), + SizedBox(height: 4.w), + Row( + children: [ + Image.asset( + 'sc_images/general/sc_icon_jb.png', + width: 28.w, + height: 28.w, + ), + SizedBox(width: 6.w), + Consumer( + builder: (context, ref, child) { + return Text( + ':${_balanceText(ref.myBalance)}', + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + ), + ); + }, + ), + ], + ), + ], + ), + ), + Image.asset( + 'sc_images/index/sc_icon_wallet_icon.png', + width: 68.w, + height: 68.w, + ), + ], + ), + ), + ); + } + + Widget _buildMenuCard2() { + final userProfile = context.watch(); + final items = <_MenuRowData>[]; + final hasCurrentUser = userProfile.currentUserProfile != null; + final isAgent = userProfile.userIdentity?.agent ?? false; + + // 1. 主播/代理入口 + if (hasCurrentUser) { + if (isAgent) { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.agentCenter, @@ -317,12 +317,12 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false", - ); - }, - ), - ); - } else { + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } else { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.hostCenter, @@ -330,17 +330,17 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false", - ); - }, - ), - ); - } - } - - // 2. BD 相关(非管理员时) - if (!(userProfile.userIdentity?.admin ?? false)) { - if (userProfile.userIdentity?.bdLeader ?? false) { + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } + } + + // 2. BD 相关(非管理员时) + if (!(userProfile.userIdentity?.admin ?? false)) { + if (userProfile.userIdentity?.bdLeader ?? false) { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.bdLeader, @@ -348,12 +348,12 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false", - ); - }, - ), - ); - } else if (userProfile.userIdentity?.bd ?? false) { + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false", + ); + }, + ), + ); + } else if (userProfile.userIdentity?.bd ?? false) { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.bdCenter, @@ -361,16 +361,16 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false", - ); - }, - ), - ); - } - } - - // 3. 货运代理 - if (userProfile.userIdentity?.freightAgent ?? false) { + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } + } + + // 3. 货运代理 + if (userProfile.userIdentity?.freightAgent ?? false) { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.rechargeAgency, @@ -378,15 +378,15 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false", - ); - }, - ), - ); - } - - // 4. 管理员(注意:你的原逻辑中管理员和上面BD逻辑有重叠,这里按原样单独处理) - if (userProfile.userIdentity?.admin ?? false) { + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false", + ); + }, + ), + ); + } + + // 4. 管理员(注意:你的原逻辑中管理员和上面BD逻辑有重叠,这里按原样单独处理) + if (userProfile.userIdentity?.admin ?? false) { items.add( _MenuRowData( title: SCAppLocalizations.of(context)!.adminCenter, @@ -394,193 +394,193 @@ class _MePage2State extends State { onTap: () { SCNavigatorUtils.push( context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false", - ); - }, - ), - ); - } - return _buildMenuCard(items); - } - - Widget _buildMenuCard(List<_MenuRowData> items) { - // 没有任何项时,不显示整个卡片 - if (items.isEmpty) return const SizedBox.shrink(); - return _buildGlassCard( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w), - radius: 4.w, - child: Column( - children: [ - for (int i = 0; i < items.length; i++) ...[_buildMenuRow(items[i])], - ], - ), - ); - } - - Widget _buildMenuRow(_MenuRowData item) { - return SCDebounceWidget( - onTap: item.onTap, - child: SizedBox( - height: 46.w, - child: Row( - children: [ - Image.asset( - item.assetIcon!, - width: 24.w, - height: 24.w, - color: Colors.white, - ), - - SizedBox(width: 12.w), - Expanded( - child: Text( - item.title, - style: TextStyle( - color: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.w600, - ), - ), - ), - Icon( - SCGlobalConfig.lang == 'ar' - ? Icons.keyboard_arrow_left - : Icons.keyboard_arrow_right, - color: Colors.white, - size: 18.w, - ), - ], - ), - ), - ); - } - - Widget _buildEntryItem({ - required String title, - required String iconPath, - required VoidCallback onTap, - }) { - return Expanded( - child: SCDebounceWidget( - onTap: onTap, - child: Container( - height: 110.w, - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'), - fit: BoxFit.fill, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset(iconPath, width: 54.w, height: 54.w), - SizedBox(height: 4.w), - Text( - title, - style: TextStyle( - color: Colors.white, - fontSize: 15.sp, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildStatItem({ - required String value, - required String label, - required VoidCallback onTap, - }) { - return Expanded( - child: SCDebounceWidget( - onTap: onTap, - child: Column( - children: [ - Text( - value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 16.sp, - fontWeight: FontWeight.w700, - ), - ), - SizedBox(height: 1.w), - Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - } - - Widget _buildStatDivider() { - return Container( - width: 0.5.w, - height: 16.w, - color: const Color(0xFFE6E6E6).withValues(alpha: 0.8), - margin: EdgeInsets.symmetric(horizontal: 6.w), - ); - } - - Widget _buildGlassCard({ - required Widget child, - required EdgeInsets padding, - required double radius, - }) { - return Container( - width: double.infinity, - padding: padding, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(radius), - border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF0A7D49).withValues(alpha: 0.20), - const Color(0xFF031513).withValues(alpha: 0.35), - ], - ), - ), - child: child, - ); - } - - String _counterText(String type) { - return _counterMap[type]?.quantity ?? '0'; - } - - String _balanceText(double balance) { - if (balance >= 1000) { - return '${(balance / 1000).toStringAsFixed(2)}k'; - } - return balance.toStringAsFixed(balance % 1 == 0 ? 0 : 2); - } -} - -class _MenuRowData { - const _MenuRowData({ - required this.title, - required this.onTap, - this.assetIcon, - }); - - final String title; - final String? assetIcon; - final VoidCallback onTap; -} + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false", + ); + }, + ), + ); + } + return _buildMenuCard(items); + } + + Widget _buildMenuCard(List<_MenuRowData> items) { + // 没有任何项时,不显示整个卡片 + if (items.isEmpty) return const SizedBox.shrink(); + return _buildGlassCard( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w), + radius: 4.w, + child: Column( + children: [ + for (int i = 0; i < items.length; i++) ...[_buildMenuRow(items[i])], + ], + ), + ); + } + + Widget _buildMenuRow(_MenuRowData item) { + return SCDebounceWidget( + onTap: item.onTap, + child: SizedBox( + height: 46.w, + child: Row( + children: [ + Image.asset( + item.assetIcon!, + width: 24.w, + height: 24.w, + color: Colors.white, + ), + + SizedBox(width: 12.w), + Expanded( + child: Text( + item.title, + style: TextStyle( + color: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_left + : Icons.keyboard_arrow_right, + color: Colors.white, + size: 18.w, + ), + ], + ), + ), + ); + } + + Widget _buildEntryItem({ + required String title, + required String iconPath, + required VoidCallback onTap, + }) { + return Expanded( + child: SCDebounceWidget( + onTap: onTap, + child: Container( + height: 110.w, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(iconPath, width: 54.w, height: 54.w), + SizedBox(height: 4.w), + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatItem({ + required String value, + required String label, + required VoidCallback onTap, + }) { + return Expanded( + child: SCDebounceWidget( + onTap: onTap, + child: Column( + children: [ + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 1.w), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatDivider() { + return Container( + width: 0.5.w, + height: 16.w, + color: const Color(0xFFE6E6E6).withValues(alpha: 0.8), + margin: EdgeInsets.symmetric(horizontal: 6.w), + ); + } + + Widget _buildGlassCard({ + required Widget child, + required EdgeInsets padding, + required double radius, + }) { + return Container( + width: double.infinity, + padding: padding, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF0A7D49).withValues(alpha: 0.20), + const Color(0xFF031513).withValues(alpha: 0.35), + ], + ), + ), + child: child, + ); + } + + String _counterText(String type) { + return _counterMap[type]?.quantity ?? '0'; + } + + String _balanceText(double balance) { + if (balance >= 1000) { + return '${(balance / 1000).toStringAsFixed(2)}k'; + } + return balance.toStringAsFixed(balance % 1 == 0 ? 0 : 2); + } +} + +class _MenuRowData { + const _MenuRowData({ + required this.title, + required this.onTap, + this.assetIcon, + }); + + final String title; + final String? assetIcon; + final VoidCallback onTap; +} diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 6106508..c0405a4 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -87,6 +87,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { Map roomWheatMap = {}; RtcEngine? engine; + RtcEngineEventHandler? _rtcEngineEventHandler; BuildContext? context; RtmProvider? rtmProvider; @@ -749,71 +750,23 @@ class RealTimeCommunicationManager extends ChangeNotifier { Future joinAgoraVoiceChannel() async { try { - engine = await _initAgoraRtcEngine(); - engine?.setAudioProfile( + final rtcEngine = await _initAgoraRtcEngine(); + rtcEngine.setAudioProfile( profile: AudioProfileType.audioProfileSpeechStandard, scenario: AudioScenarioType.audioScenarioGameStreaming, ); - engine?.enableAudioVolumeIndication( + rtcEngine.enableAudioVolumeIndication( interval: 500, smooth: 3, reportVad: true, ); - await engine?.disableVideo(); - await engine?.setLocalPublishFallbackOption( - StreamFallbackOptions.streamFallbackOptionAudioOnly, - ); - engine?.registerEventHandler( - RtcEngineEventHandler( - onError: (ErrorCodeType err, String msg) { - print('rtc错误${err}'); - }, - onAudioMixingStateChanged: ( - AudioMixingStateType state, - AudioMixingReasonType reason, - ) { - switch (state) { - case AudioMixingStateType.audioMixingStatePlaying: - isMusicPlaying = true; - break; - case AudioMixingStateType.audioMixingStateStopped: - case AudioMixingStateType.audioMixingStateFailed: - isMusicPlaying = false; - break; - default: - break; - } - }, - onJoinChannelSuccess: (RtcConnection connection, int elapsed) { - print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); - }, - onUserJoined: (connection, remoteUid, elapsed) { - print('rtc用户 $remoteUid 加入了频道'); - }, - // 监听远端用户离开 - onUserOffline: (connection, remoteUid, reason) { - print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})'); - }, - onTokenPrivilegeWillExpire: ( - RtcConnection connection, - String token, - ) async { - var rtcToken = await SCAccountRepository().getRtcToken( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - isPublisher: isOnMai(), - ); - engine?.renewToken(rtcToken.rtcToken ?? ""); - }, - onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, - ), - ); + await rtcEngine.disableVideo(); var rtcToken = await SCAccountRepository().getRtcToken( currenRoom?.roomProfile?.roomProfile?.id ?? "", AccountStorage().getCurrentUser()?.userProfile?.id ?? "", isPublisher: false, ); - await engine?.joinChannel( + await rtcEngine.joinChannel( token: rtcToken.rtcToken ?? "", channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "", uid: _resolveAgoraUidForCurrentUser(), @@ -832,7 +785,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { ), ); _syncSelfMicRuntimeState(); - engine?.muteAllRemoteAudioStreams(roomIsMute); + rtcEngine.muteAllRemoteAudioStreams(roomIsMute); } catch (e) { SCTts.show("Join room fail"); exitCurrentVoiceRoomSession(false); @@ -841,14 +794,85 @@ class RealTimeCommunicationManager extends ChangeNotifier { } Future _initAgoraRtcEngine() async { - RtcEngine? engine; - while (engine == null) { - engine = createAgoraRtcEngine(); - await engine.initialize( - RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), - ); + if (engine != null) { + return engine!; + } + final rtcEngine = createAgoraRtcEngine(); + await rtcEngine.initialize( + RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), + ); + engine = rtcEngine; + _rtcEngineEventHandler = RtcEngineEventHandler( + onError: (ErrorCodeType err, String msg) { + print('rtc错误$err'); + }, + onAudioMixingStateChanged: ( + AudioMixingStateType state, + AudioMixingReasonType reason, + ) { + switch (state) { + case AudioMixingStateType.audioMixingStatePlaying: + isMusicPlaying = true; + break; + case AudioMixingStateType.audioMixingStateStopped: + case AudioMixingStateType.audioMixingStateFailed: + isMusicPlaying = false; + break; + default: + break; + } + }, + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); + }, + onUserJoined: (connection, remoteUid, elapsed) { + print('rtc用户 $remoteUid 加入了频道'); + }, + // 监听远端用户离开 + onUserOffline: (connection, remoteUid, reason) { + print('rtc用户 $remoteUid 离开了频道 (原因: $reason)'); + }, + onTokenPrivilegeWillExpire: ( + RtcConnection connection, + String token, + ) async { + var rtcToken = await SCAccountRepository().getRtcToken( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + isPublisher: isOnMai(), + ); + engine?.renewToken(rtcToken.rtcToken ?? ""); + }, + onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, + ); + rtcEngine.registerEventHandler(_rtcEngineEventHandler!); + return rtcEngine; + } + + Future releaseRtcEngineForAppTermination() async { + final rtcEngine = engine; + if (rtcEngine == null) { + return; + } + + final eventHandler = _rtcEngineEventHandler; + engine = null; + _rtcEngineEventHandler = null; + + try { + if (eventHandler != null) { + rtcEngine.unregisterEventHandler(eventHandler); + } + await rtcEngine.leaveChannel(); + } catch (e) { + print('rtc销毁前离开频道出错: $e'); + } + + try { + await rtcEngine.release(); + } catch (e) { + print('rtc释放引擎出错: $e'); } - return engine; } void initializeAudioVolumeIndicationCallback( @@ -1291,7 +1315,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { rtmProvider?.cleanRoomData(); await engine?.leaveChannel(); await Future.delayed(Duration(milliseconds: 100)); - await engine?.release(); await rtmProvider?.quitGroup( currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "", ); @@ -1325,11 +1348,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { } await engine?.leaveChannel(); await Future.delayed(const Duration(milliseconds: 100)); - await engine?.release(); } catch (e) { print('rtc清理本地房间状态出错: $e'); } finally { - engine = null; rtmProvider?.cleanRoomData(); _clearData(); notifyListeners(); @@ -1339,7 +1360,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///清空列表数据 void _clearData() { _stopRoomStatePolling(); - engine = null; _resetHeartbeatTracking(); _resetLocalAudioRuntimeTracking(); _disableMicListRefreshForCurrentSession = false; @@ -1370,6 +1390,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); } + @override + void dispose() { + _stopRoomStatePolling(); + unawaited(releaseRtcEngineForAppTermination()); + super.dispose(); + } + void toggleRemoteAudioMuteForAllUsers() { roomIsMute = !roomIsMute; engine?.muteAllRemoteAudioStreams(roomIsMute); diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index f4d5bba..3acca6c 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -15,6 +15,7 @@ import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widg import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; typedef FloatingScreenManager = OverlayManager; @@ -79,6 +80,19 @@ class OverlayManager { logicalHeight: 96, ), ); + if (message.type == 0) { + unawaited(_warmLuckyGiftBannerSvga()); + } + } + + Future _warmLuckyGiftBannerSvga() async { + try { + await SCSvgaAssetWidget.preloadAsset( + FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath, + ); + } catch (error) { + debugPrint('预热幸运礼物横幅SVGA失败: $error'); + } } void _safeScheduleNext() { diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index 389825b..df1e0e6 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -1,37 +1,45 @@ +import 'dart:async'; +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svga/flutter_svga.dart'; import 'package:provider/provider.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/main.dart'; -import 'package:marquee/marquee.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; class FloatingLuckGiftScreenWidget extends StatefulWidget { + static const String backgroundSvgaAssetPath = + "sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga"; + final SCFloatingMessage message; final VoidCallback onAnimationCompleted; // 动画完成回调 const FloatingLuckGiftScreenWidget({ - Key? key, + super.key, required this.message, required this.onAnimationCompleted, - }) : super(key: key); + }); @override - _FloatingLuckGiftScreenWidgetState createState() => + State createState() => _FloatingLuckGiftScreenWidgetState(); } class _FloatingLuckGiftScreenWidgetState extends State with TickerProviderStateMixin { - static const String _coinIconAssetPath = "sc_images/general/sc_icon_jb.png"; + static const String _avatarDynamicKey = "Avatar"; + static const String _textDynamicKey = "test"; + static const String _multipleDynamicKey = "img_18"; late AnimationController _controller; late Animation _offsetAnimation; late AnimationController _swipeController; // 新增:滑动动画控制器 @@ -158,196 +166,221 @@ class _FloatingLuckGiftScreenWidgetState alignment: Alignment.center, height: 83.w, width: 350.w, - child: Stack( - children: [ - Transform.flip( - flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 - flipY: false, // 垂直翻转设为 false - child: Image.asset( - "sc_images/room/sc_icon_luck_gift_float_n_bg.png", - fit: BoxFit.fill, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - Container( - margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), - child: netImage( - url: widget.message.userAvatarUrl ?? "", - width: 52.w, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Expanded( - child: Container( - margin: EdgeInsets.only(top: 10.w), - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 85.w, - maxHeight: 20.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: "${widget.message.userName} ", - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - letterSpacing: 0.1, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: - CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration( - seconds: 1, - ), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - "${widget.message.userName} ", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - SizedBox(width: 3.w), - Expanded(child: _buildRewardLine(context)), - SizedBox(width: 6.w), - Container( - width: 80.w, - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 2.w), - buildNumForGame( - "${widget.message.multiple ?? 0}", - size: 25.w, - ), - SizedBox(height: 3.w), - SCGlobalConfig.lang == "ar" - ? Image.asset( - "sc_images/room/sc_icon_times_text_ar.png", - height: 12.w, - ) - : Image.asset( - "sc_images/room/sc_icon_times_text_en.png", - height: 12.w, - ), - ], - ), - ), - SizedBox(width: 6.w), - ], - ), - ), - ), - ], - ), - ], + child: Transform.flip( + flipX: SCGlobalConfig.lang == "ar" ? true : false, + flipY: false, + child: SCSvgaAssetWidget( + assetPath: FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath, + width: 350.w, + height: 83.w, + fit: BoxFit.fill, + dynamicIdentity: [ + widget.message.userAvatarUrl ?? "", + widget.message.userName ?? "", + "${widget.message.coins ?? 0}", + widget.message.giftUrl ?? "", + "${widget.message.multiple ?? 0}", + ].join("|"), + movieConfigurer: _configureLuckGiftSvga, + ), ), ), ), ); } - Widget _buildRewardLine(BuildContext context) { - final baseStyle = TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 0.1, - decoration: TextDecoration.none, - ); - final amountStyle = baseStyle.copyWith(color: const Color(0xffFEF129)); - return Text.rich( - TextSpan( - children: [ - TextSpan(text: SCAppLocalizations.of(context)!.get, style: baseStyle), - TextSpan( - text: " ${_formatCoins(widget.message.coins)} ", - style: amountStyle, - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: EdgeInsetsDirectional.only(end: 3.w), - child: Image.asset(_coinIconAssetPath, width: 16.w, height: 16.w), - ), - ), - TextSpan(text: "from ", style: baseStyle), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: _buildGiftIcon(context), - ), - ], - ), - textAlign: TextAlign.start, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - strutStyle: StrutStyle( - height: 1.1, - fontWeight: FontWeight.bold, - forceStrutHeight: true, - ), - ); - } - - Widget _buildGiftIcon(BuildContext context) { - final primaryGiftUrl = (widget.message.giftUrl ?? "").trim(); - final fallbackGiftUrl = _resolveFallbackGiftUrl(context); - final displayGiftUrl = - primaryGiftUrl.isNotEmpty ? primaryGiftUrl : fallbackGiftUrl; - if (displayGiftUrl.isEmpty) { - return _buildGiftIconPlaceholder(); + Future _configureLuckGiftSvga(MovieEntity movieEntity) async { + final avatarUrl = (widget.message.userAvatarUrl ?? "").trim(); + if (avatarUrl.isNotEmpty) { + try { + final avatarImage = await _loadUiImage( + avatarUrl, + logicalWidth: 64, + logicalHeight: 64, + ); + if (avatarImage != null) { + movieEntity.dynamicItem.setImage(avatarImage, _avatarDynamicKey); + } + } catch (error) { + debugPrint("[LuckGiftSVGA] avatar sync failed: $error"); + } } - final backupUrl = - fallbackGiftUrl.isNotEmpty && fallbackGiftUrl != displayGiftUrl - ? fallbackGiftUrl - : ""; + try { + final textImage = await _buildBannerTextImage(); + movieEntity.dynamicItem.setImage(textImage, _textDynamicKey); + } catch (error) { + debugPrint("[LuckGiftSVGA] text sync failed: $error"); + } - return netImage( - url: displayGiftUrl, - width: 18.w, - height: 18.w, - borderRadius: BorderRadius.circular(3.w), - loadingWidget: _buildGiftIconPlaceholder(), - errorWidget: - backupUrl.isNotEmpty - ? netImage( - url: backupUrl, - width: 18.w, - height: 18.w, - borderRadius: BorderRadius.circular(3.w), - noDefaultImg: true, - loadingWidget: _buildGiftIconPlaceholder(), - errorWidget: _buildGiftIconPlaceholder(), - ) - : _buildGiftIconPlaceholder(), + final multipleValue = widget.message.multiple ?? 0; + if (multipleValue > 0) { + final numberPainter = TextPainter( + textDirection: TextDirection.ltr, + text: TextSpan( + text: multipleValue.toString(), + style: const TextStyle( + fontSize: 36, + color: Color(0xffFFDC4A), + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + height: 1, + decoration: TextDecoration.none, + ), + ), + )..layout(); + final labelPainter = TextPainter( + textDirection: TextDirection.ltr, + text: const TextSpan( + text: "times", + style: TextStyle( + fontSize: 28, + color: Color(0xffFFDC4A), + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + height: 1, + decoration: TextDecoration.none, + ), + ), + )..layout(); + movieEntity.dynamicItem.setDynamicDrawer((canvas, frameIndex) { + const centerX = 580.0; + numberPainter.paint( + canvas, + Offset(centerX - (numberPainter.width / 2), 18), + ); + labelPainter.paint( + canvas, + Offset(centerX - (labelPainter.width / 2), 49), + ); + }, _multipleDynamicKey); + } + } + + Future _buildBannerTextImage() async { + const double logicalWidth = 356; + const double logicalHeight = 42; + const double pixelRatio = 3.0; + + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(pixelRatio, pixelRatio); + canvas.clipRect(const Rect.fromLTWH(0, 0, logicalWidth, logicalHeight)); + + final baseStyle = TextStyle( + fontSize: 25, + color: Colors.white, + fontWeight: FontWeight.w800, + letterSpacing: 0.1, + height: 1, + decoration: TextDecoration.none, + ); + final highlightStyle = baseStyle.copyWith( + color: const Color(0xffFEF129), + fontWeight: FontWeight.w900, + ); + + final l10n = SCAppLocalizations.of(context)!; + final userName = (widget.message.userName ?? "").trim(); + final textPainter = TextPainter( + textDirection: TextDirection.ltr, + maxLines: 1, + ellipsis: "…", + text: TextSpan( + children: [ + if (userName.isNotEmpty) + TextSpan(text: "$userName ", style: highlightStyle), + TextSpan(text: l10n.get, style: baseStyle), + TextSpan( + text: " ${_formatCoins(widget.message.coins)} ", + style: highlightStyle, + ), + TextSpan(text: "from ", style: baseStyle), + ], + ), + )..layout(maxWidth: logicalWidth - 40); + + const double startX = 6; + final textY = (logicalHeight - textPainter.height) / 2; + textPainter.paint(canvas, Offset(startX, textY)); + + double contentEndX = startX + textPainter.width; + + final giftImage = await _loadUiImage( + _resolveDisplayGiftUrl(context), + logicalWidth: 108, + logicalHeight: 108, + ); + if (giftImage != null) { + const giftSize = 36.0; + final giftRect = Rect.fromLTWH( + contentEndX, + (logicalHeight - giftSize) / 2, + giftSize, + giftSize, + ); + paintImage( + canvas: canvas, + rect: giftRect, + image: giftImage, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ); + } + + final picture = recorder.endRecording(); + return picture.toImage( + (logicalWidth * pixelRatio).round(), + (logicalHeight * pixelRatio).round(), ); } - String _resolveFallbackGiftUrl(BuildContext context) { + Future _loadUiImage( + String resource, { + double? logicalWidth, + double? logicalHeight, + }) async { + final target = resource.trim(); + if (target.isEmpty) { + return null; + } + final provider = buildCachedImageProvider( + target, + logicalWidth: logicalWidth, + logicalHeight: logicalHeight, + ); + final stream = provider.resolve(ImageConfiguration.empty); + final completer = Completer(); + late final ImageStreamListener listener; + listener = ImageStreamListener( + (imageInfo, synchronousCall) { + if (!completer.isCompleted) { + completer.complete(imageInfo.image); + } + stream.removeListener(listener); + }, + onError: (Object error, StackTrace? stackTrace) { + if (!completer.isCompleted) { + completer.complete(null); + } + stream.removeListener(listener); + }, + ); + stream.addListener(listener); + return completer.future.timeout( + const Duration(seconds: 2), + onTimeout: () { + stream.removeListener(listener); + return null; + }, + ); + } + + String _resolveDisplayGiftUrl(BuildContext context) { + final primaryGiftUrl = (widget.message.giftUrl ?? "").trim(); + if (primaryGiftUrl.isNotEmpty) { + return primaryGiftUrl; + } final giftId = (widget.message.giftId ?? "").trim(); if (giftId.isEmpty) { return ""; @@ -359,10 +392,6 @@ class _FloatingLuckGiftScreenWidgetState return (gift?.giftPhoto ?? "").trim(); } - Widget _buildGiftIconPlaceholder() { - return SizedBox(width: 18.w, height: 18.w); - } - String _formatCoins(num? coins) { final value = (coins ?? 0); if (value > 9999) { @@ -373,160 +402,4 @@ class _FloatingLuckGiftScreenWidgetState } return value.toString(); } - - ///礼物总价钱达到10000 - Widget _buildGiftAnimation() { - return GestureDetector( - onTap: () { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); - } - }, - ); - }, - onHorizontalDragEnd: (details) { - // 获取当前的文本方向 - final textDirection = Directionality.of(context); - - // 定义一个根据方向转换速度符号的辅助函数 - double effectiveVelocity(double velocity) { - // 在RTL模式下,反转速度的正负号 - return textDirection == TextDirection.rtl ? -velocity : velocity; - } - - double velocity = effectiveVelocity(details.primaryVelocity ?? 0); - if (velocity < 0) { - // 向左滑动,把当前视图向左平移出去 - _handleSwipeLeft(); - } - }, - child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, - child: Container( - alignment: Alignment.center, - height: 50.w, - width: 290.w, - margin: EdgeInsets.only(top: 20.w), - padding: EdgeInsets.symmetric(horizontal: 15.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), - fit: BoxFit.fill, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - // 可以根据消息模型丰富内容,例如显示头像 - if (widget.message.userAvatarUrl?.isNotEmpty ?? false) - head(url: widget.message.userAvatarUrl ?? "", width: 42.w), - SizedBox(width: 4.w), - Expanded( - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.userName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.userName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - text( - SCAppLocalizations.of(context)!.sendTo, - textColor: Colors.white, - fontSize: 13.sp, - fontWeight: FontWeight.bold, - ), - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.toUserName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.toUserName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.toUserName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - ], - ), - ), - netImage(url: widget.message.giftUrl ?? "", width: 32.w), - SizedBox(width: 3.w), - Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), - buildNum("${widget.message.number}", size: 18.w), - ], - ), - ), - ), - ); - } } diff --git a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart index 94ffd0f..eb006e2 100644 --- a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart @@ -1,8 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svga/flutter_svga.dart'; +typedef SCSvgaMovieConfigurer = + FutureOr Function(MovieEntity movieEntity); + class SCSvgaAssetWidget extends StatefulWidget { + static final Map _cache = {}; + static final Map> _loadingTasks = + >{}; + const SCSvgaAssetWidget({ super.key, required this.assetPath, @@ -17,6 +26,8 @@ class SCSvgaAssetWidget extends StatefulWidget { this.clearsAfterStop = false, this.onPlaybackStarted, this.onPlaybackCompleted, + this.dynamicIdentity, + this.movieConfigurer, }); final String assetPath; @@ -31,80 +42,14 @@ class SCSvgaAssetWidget extends StatefulWidget { final bool clearsAfterStop; final VoidCallback? onPlaybackStarted; final VoidCallback? onPlaybackCompleted; + final Object? dynamicIdentity; + final SCSvgaMovieConfigurer? movieConfigurer; - @override - State createState() => _SCSvgaAssetWidgetState(); -} - -class _SCSvgaAssetWidgetState extends State - with SingleTickerProviderStateMixin { - static final Map _cache = {}; - static final Map> _loadingTasks = - >{}; - - late final SVGAAnimationController _controller; - String? _loadedAssetPath; - bool _hasError = false; - - @override - void initState() { - super.initState(); - _controller = SVGAAnimationController(vsync: this); - _controller.addStatusListener(_handleAnimationStatusChanged); - _loadAsset(); + static Future preloadAsset(String assetPath) { + return _obtainMovieEntity(assetPath); } - void _handleAnimationStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.forward) { - widget.onPlaybackStarted?.call(); - return; - } - if (status == AnimationStatus.completed) { - widget.onPlaybackCompleted?.call(); - } - } - - @override - void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.assetPath != widget.assetPath) { - _loadAsset(); - return; - } - if (oldWidget.active != widget.active || oldWidget.loop != widget.loop) { - _syncPlayback( - restartIfActive: widget.active && oldWidget.active != widget.active, - ); - } - } - - Future _loadAsset() async { - final assetPath = widget.assetPath; - setState(() { - _hasError = false; - }); - try { - final movieEntity = await _obtainMovieEntity(assetPath); - if (!mounted || widget.assetPath != assetPath) { - return; - } - setState(() { - _loadedAssetPath = assetPath; - _controller.videoItem = movieEntity; - }); - _syncPlayback(restartIfActive: true); - } catch (error) { - debugPrint('[SCSVGA] load failed asset=$assetPath error=$error'); - if (!mounted || widget.assetPath != assetPath) { - return; - } - setState(() { - _hasError = true; - }); - } - } - - Future _obtainMovieEntity(String assetPath) async { + static Future _obtainMovieEntity(String assetPath) async { final cached = _cache[assetPath]; if (cached != null) { return cached; @@ -138,6 +83,79 @@ class _SCSvgaAssetWidgetState extends State } } + @override + State createState() => _SCSvgaAssetWidgetState(); +} + +class _SCSvgaAssetWidgetState extends State + with SingleTickerProviderStateMixin { + late final SVGAAnimationController _controller; + String? _loadedAssetPath; + bool _hasError = false; + + @override + void initState() { + super.initState(); + _controller = SVGAAnimationController(vsync: this); + _controller.addStatusListener(_handleAnimationStatusChanged); + _loadAsset(); + } + + void _handleAnimationStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.forward) { + widget.onPlaybackStarted?.call(); + return; + } + if (status == AnimationStatus.completed) { + widget.onPlaybackCompleted?.call(); + } + } + + @override + void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.assetPath != widget.assetPath || + oldWidget.dynamicIdentity != widget.dynamicIdentity) { + _loadAsset(); + return; + } + if (oldWidget.active != widget.active || oldWidget.loop != widget.loop) { + _syncPlayback( + restartIfActive: widget.active && oldWidget.active != widget.active, + ); + } + } + + Future _loadAsset() async { + final assetPath = widget.assetPath; + setState(() { + _hasError = false; + }); + try { + final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath); + if (widget.movieConfigurer != null) { + movieEntity.dynamicItem.reset(); + await widget.movieConfigurer!(movieEntity); + } + if (!mounted || widget.assetPath != assetPath) { + return; + } + setState(() { + _loadedAssetPath = assetPath; + _controller.videoItem = movieEntity; + }); + _syncPlayback(restartIfActive: true); + } catch (error) { + debugPrint('[SCSVGA] load failed asset=$assetPath error=$error'); + if (!mounted || widget.assetPath != assetPath) { + return; + } + setState(() { + _hasError = true; + }); + } + } + void _syncPlayback({bool restartIfActive = false}) { if (_controller.videoItem == null) { return; diff --git a/pubspec.yaml b/pubspec.yaml index a5ecc80..f4697b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.0+4 +version: 1.2.1+5 environment: diff --git a/sc_images/general/sc_icon_jb.png b/sc_images/general/sc_icon_jb.png index 8d2e4f6..cce104f 100644 Binary files a/sc_images/general/sc_icon_jb.png and b/sc_images/general/sc_icon_jb.png differ diff --git a/sc_images/general/sc_icon_jb2.png b/sc_images/general/sc_icon_jb2.png index b33dbe0..c69c622 100644 Binary files a/sc_images/general/sc_icon_jb2.png and b/sc_images/general/sc_icon_jb2.png differ diff --git a/sc_images/general/sc_icon_jb3.png b/sc_images/general/sc_icon_jb3.png index d572ec1..1f50345 100644 Binary files a/sc_images/general/sc_icon_jb3.png and b/sc_images/general/sc_icon_jb3.png differ diff --git a/sc_images/level/sc_icon_user_level_10_20.png b/sc_images/level/sc_icon_user_level_10_20.png index bdbc184..3c4d9e8 100644 Binary files a/sc_images/level/sc_icon_user_level_10_20.png and b/sc_images/level/sc_icon_user_level_10_20.png differ diff --git a/sc_images/level/sc_icon_user_level_1_10.png b/sc_images/level/sc_icon_user_level_1_10.png index 2bed87a..0880a04 100644 Binary files a/sc_images/level/sc_icon_user_level_1_10.png and b/sc_images/level/sc_icon_user_level_1_10.png differ diff --git a/sc_images/level/sc_icon_user_level_20_30.png b/sc_images/level/sc_icon_user_level_20_30.png index 0c7f08d..195773c 100644 Binary files a/sc_images/level/sc_icon_user_level_20_30.png and b/sc_images/level/sc_icon_user_level_20_30.png differ diff --git a/sc_images/level/sc_icon_user_level_30_40.png b/sc_images/level/sc_icon_user_level_30_40.png index 06a8eb5..a488d8a 100644 Binary files a/sc_images/level/sc_icon_user_level_30_40.png and b/sc_images/level/sc_icon_user_level_30_40.png differ diff --git a/sc_images/level/sc_icon_user_level_40_50.png b/sc_images/level/sc_icon_user_level_40_50.png index de5458c..776b71c 100644 Binary files a/sc_images/level/sc_icon_user_level_40_50.png and b/sc_images/level/sc_icon_user_level_40_50.png differ diff --git a/sc_images/level/sc_icon_user_level_center_bg_1.png b/sc_images/level/sc_icon_user_level_center_bg_1.png index 783d2ee..84392b1 100644 Binary files a/sc_images/level/sc_icon_user_level_center_bg_1.png and b/sc_images/level/sc_icon_user_level_center_bg_1.png differ diff --git a/sc_images/level/sc_icon_user_wealth_center_bg_1.png b/sc_images/level/sc_icon_user_wealth_center_bg_1.png index abe8399..015572d 100644 Binary files a/sc_images/level/sc_icon_user_wealth_center_bg_1.png and b/sc_images/level/sc_icon_user_wealth_center_bg_1.png differ diff --git a/sc_images/level/sc_icon_wealth_level_10_20.png b/sc_images/level/sc_icon_wealth_level_10_20.png index 1295a44..eaa1227 100644 Binary files a/sc_images/level/sc_icon_wealth_level_10_20.png and b/sc_images/level/sc_icon_wealth_level_10_20.png differ diff --git a/sc_images/level/sc_icon_wealth_level_1_10.png b/sc_images/level/sc_icon_wealth_level_1_10.png index 24cb2bf..2095706 100644 Binary files a/sc_images/level/sc_icon_wealth_level_1_10.png and b/sc_images/level/sc_icon_wealth_level_1_10.png differ diff --git a/sc_images/level/sc_icon_wealth_level_20_30.png b/sc_images/level/sc_icon_wealth_level_20_30.png index c08b3b2..80d7fcc 100644 Binary files a/sc_images/level/sc_icon_wealth_level_20_30.png and b/sc_images/level/sc_icon_wealth_level_20_30.png differ diff --git a/sc_images/level/sc_icon_wealth_level_30_40.png b/sc_images/level/sc_icon_wealth_level_30_40.png index 4e894f6..525ebc8 100644 Binary files a/sc_images/level/sc_icon_wealth_level_30_40.png and b/sc_images/level/sc_icon_wealth_level_30_40.png differ diff --git a/sc_images/level/sc_icon_wealth_level_40_50.png b/sc_images/level/sc_icon_wealth_level_40_50.png index ce2bd23..1f8bff5 100644 Binary files a/sc_images/level/sc_icon_wealth_level_40_50.png and b/sc_images/level/sc_icon_wealth_level_40_50.png differ diff --git a/sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga b/sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga new file mode 100644 index 0000000..8daa994 Binary files /dev/null and b/sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga differ diff --git a/test/_tmp_dump_svga_catalog_test.dart b/test/_tmp_dump_svga_catalog_test.dart new file mode 100644 index 0000000..6e71fa9 --- /dev/null +++ b/test/_tmp_dump_svga_catalog_test.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_svga/src/proto/svga.pbserver.dart'; + +void main() { + test('dump svga catalog', () async { + const path = + 'sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga'; + final rawBytes = File(path).readAsBytesSync(); + final inflatedBytes = const ZLibDecoder().decodeBytes(rawBytes); + final movie = MovieEntity.fromBuffer(inflatedBytes); + final outputDir = Directory('/tmp/luck_gift_svga_catalog'); + outputDir.createSync(recursive: true); + + final metadata = >[]; + for (final key in movie.images.keys) { + final bytes = movie.images[key]!; + File('${outputDir.path}/$key.png').writeAsBytesSync(bytes); + + final sprites = + movie.sprites.where((sprite) => sprite.imageKey == key).toList(); + final firstFrame = sprites.isNotEmpty && sprites.first.frames.isNotEmpty + ? sprites.first.frames.first + : null; + metadata.add({ + 'key': key, + 'bytes': bytes.length, + 'spriteCount': sprites.length, + 'layout': firstFrame == null + ? null + : { + 'x': firstFrame.layout.x, + 'y': firstFrame.layout.y, + 'width': firstFrame.layout.width, + 'height': firstFrame.layout.height, + }, + 'transform': firstFrame == null + ? null + : { + 'a': firstFrame.transform.a, + 'd': firstFrame.transform.d, + 'tx': firstFrame.transform.tx, + 'ty': firstFrame.transform.ty, + }, + }); + } + + File('${outputDir.path}/metadata.json').writeAsStringSync( + const JsonEncoder.withIndent(' ').convert(metadata), + ); + }); +} diff --git a/需求进度.md b/需求进度.md index cc94db6..807b815 100644 --- a/需求进度.md +++ b/需求进度.md @@ -86,6 +86,17 @@ - 已继续绕开幸运礼物紫色横幅里可疑的小图 `ExtendedImage` 渲染链:当前礼物图标已改为在 `initState` 里先构建 `buildCachedImageProvider`,再通过基础 `Image(image: provider)` 直接绘制,并额外输出一次 `gift icon frame ready` 日志,用于确认这张图是否真的完成首帧解码显示;这样可以把问题进一步收敛为“图片 provider/解码”还是“组件布局/视觉观感”。 - 已按 2026-04-21 最新联调请求,把紫色幸运礼物横幅 `from` 后的图标临时替换为金币图标:这一步只用于快速验证该图标位本身的布局和可见性是否正常,不再受当前礼物图资源链路影响;如果金币图标能稳定显示,就说明问题仍集中在礼物图渲染链,而不是这个位置被遮挡或根本没画出来。 - 已按 2026-04-21 最新回归把 [floating_luck_gift_screen_widget] 的样式撤回到最初实现,不再继续在幸运礼物飘屏横幅上做偏题调试;同时已定位到真正缺图的是消息栏里的 `gameLuckyGift_5` 高亮消息,这条消息此前只写入了 `awardAmount/user/msg`,没有把 `gift` 一并塞进去,导致 `room_msg_item.dart` 读取 `widget.msg.gift?.giftPhoto` 时天然为空。当前已在 `RTM` 构造高亮 lucky 消息时补回 `giftPhoto`,让消息栏里的紫色 lucky message 能和其它消息一样拿到礼物图。 +- 已按 2026-04-23 最新横幅替换需求,把房间顶部幸运礼物飘屏横幅的背景层从静态 `sc_icon_luck_gift_float_n_bg.png` 切到本地 `sc_images/room/anim/luck_gift/kojuyee_reback_banner.svga`,并补上 `SVGA` 预热,尽量减少首次出现时先闪旧底图或首帧等待的体感。 +- 已在 2026-04-23 继续把这份 `SVGA` 做到可用级“解包”排查:当前已确认它属于 `zlib + protobuf` 结构,不是传统 zip 包;已从运行时结构里定位出 `Avatar` 和 `test` 两个可替换图片层,其中 `Avatar` 可直接承载头像,`test` 可承载主文案位。现已把幸运礼物顶部横幅改为优先把头像和主文案直接同步进这两个 `SVGA` 图层,避免外层 Flutter 叠加内容与 `SVGA` 自身位移不同步而越播越偏;同时保留旧版 `png + Row` 作为失败兜底,方便继续联调与回退。 +- 已按 2026-04-23 最新验收反馈继续收口幸运礼物顶部横幅:当前已彻底移除这条横幅里的旧静态 `png` 兜底,不再在 `SVGA` 加载期或失败时回落显示原版静态图;同时已把 `test` 文案层的绘制尺寸、字号和内边距重新放大,避免文案缩成左侧一小条,后续继续围绕 `SVGA` 动态层本身微调,不再回退到外层静态叠图方案。 +- 已按 2026-04-23 最新联调继续补齐幸运礼物顶部横幅的动态层内容:当前 `test` 图层不再只画主文案,而是会一起合成 `用户名 + get 金额 + from + 礼物图标 + 右侧倍数`;同时 `SVGA` 播放组件改回复用预热后的缓存实体,并在同一实例上重置/重灌动态层,减少每次重解整份 `SVGA` 带来的短暂空白,尽量让横幅保持“触发即播”的体感。 +- 已按 2026-04-23 最新验收继续放大幸运礼物顶部横幅的动态文案层:当前 `test` 图层的主文案字号、礼物图标尺寸、右侧倍数字号以及整体承载高度都已继续上调,优先解决“能看见但看不清”的问题;本轮不改触发链路和数据口径,只做可读性增强。 +- 已按 2026-04-23 最新验收继续微调幸运礼物顶部横幅尾部区域:当前礼物图标已继续放大,右侧倍数与 `times` 文案也同步加大,并整体右贴到横幅可用区域的最末端,优先吃掉此前尾部残留空位,让右侧视觉重心更贴稿。 +- 已按 2026-04-23 最新验收把幸运礼物顶部横幅的右侧倍数从 `test` 文案图层里独立出来,改挂到整条横幅的全宽动态图层上单独绘制:这样右侧倍数不再受原文案层宽度限制,可以真正贴到横幅最末端;同时 `from` 后面的礼物图标也继续加大并缩短与文案的间距,优先解决“图标仍偏小、尾部空位仍过多”的问题。 +- 已按 2026-04-23 最新验收继续修正幸运礼物顶部横幅右侧倍数“飞位”问题:当前已撤回那套不稳定的全宽动态图层定位,改为把倍数作为横幅容器内的独立右贴叠层直接固定到末端,优先确保位置稳定;同时 `from` 后面的礼物图标已按最新要求继续放大到更接近 `36` 的视觉尺寸,并进一步压缩与主文案的间距。 +- 已按 2026-04-23 最新验收继续收口幸运礼物顶部横幅倍数的“时机不同步”问题:当前已撤回外层独立叠层做法,重新把倍数挂回 `SVGA` 主体条带自身的动态图层坐标系里,优先保证它和横幅本体共用同一条进场/位移时间轴,不再出现横幅在飘、倍数单独漂在上方的现象;礼物图标则继续维持 `36` 视觉尺寸并贴近 `from` 后侧。 +- 已按 2026-04-23 最新验收继续微调幸运礼物顶部横幅右侧倍数的排版对齐:当前不再让上方数字和下方 `times` 各自按右边缘贴齐,而是改为共用同一条水平中心线绘制,优先修正“数字偏右、和下方文案看起来不在一列”的问题。 +- 已按 2026-04-23 验收收尾要求移除房间右侧的幸运礼物临时验收入口:此前仅用于本地预览顶部 lucky gift 横幅的“测”按钮和本地造消息逻辑已从 `room_play_widget.dart / room_game_bottom_sheet.dart` 删除,房间侧边区恢复只保留正式游戏入口;幸运礼物横幅本身的 `SVGA`、动态头像/文案/倍数链路不做回退或改动。 - 已按 2026-04-21 最新特效需求继续调整幸运礼物 `burst`:在 `luck_gift_reward_burst.svga` 下方那块淡色圆角矩形区域,补上了与 `SVGA` 同步进场/消失的中奖倍数文案;当前会和中央金额文本共用同一显隐时机,并在 `multiple > 0` 时展示为 `xN`,避免出现 `x0` 这类无效信息。 - 已继续按 2026-04-21 最新特效稿给幸运礼物 `burst` 补上中奖发送者头像:当前头像会出现在你标的黑块位置附近,并和 `SVGA` 使用同一套开始/结束显隐时机同步出现与消失;头像显示只读取当前中奖事件的 `userAvatar`,不改现有 `burst` 触发条件和播放时长。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。