diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 76d05b1..6ebc174 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -346,15 +346,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9 + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f - audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242 - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e - firebase_auth: 5342db41af2ba5ed32a6177d9e326eecbebda912 - firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c - firebase_crashlytics: 986773c0c7ea4c3f5a4ead141b58522fcd9dfdb1 + firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b + firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 + firebase_crashlytics: 30dcd6dfd2fe895c0848af46722a4227346c19aa FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd @@ -366,47 +366,47 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f - google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063 - image_cropper: a3291c624a953049bc6a02e1f8c8ceb162a24b25 - image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b - in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a + image_cropper: 655b3ba703c9e15e3111e79151624d6154288774 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8 libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - on_audio_query_ios: e5afb0d4b42cfa3e8762d8ec1953540cb4f02100 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - pag: ff30ba5e4be59380a5ddac6b183905729be15a3c - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 - sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 - tancent_vap: eceaaf095167d102727cf799c75891057834c311 - tencent_cloud_chat_sdk: 546bb906bbe585814e3d2d33afdb91f09c0eac44 - tencent_rtc_sdk: 1ca5201f7bddc691a3a95486325ad6dc792a273d + tancent_vap: fa8ad93814a9f950514a7074c662d2c937084c68 + tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790 + tencent_rtc_sdk: f77558b6b436a149d378557c2a837f73c09061bc TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 TXCustomBeautyProcesserPlugin: 099393b941cb40eda12b3a80bf6c0319957b1cfd TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2 TXLiteAVSDK_Professional: e792e6084a208ecef6196c5f28237ad8078b4d7c - url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa - video_player_avfoundation: e54d03b3b19f64f2c557354f72ce538387541ba6 - video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 - wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e - webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 + video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894 diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index c252075..c31ecf2 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -16,8 +16,8 @@ class SCVariant1Config implements AppConfig { @override String get apiHost => const String.fromEnvironment( 'API_HOST', - defaultValue: 'http://192.168.110.66:1100/', - ); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/” + defaultValue: 'http://192.168.110.64:1100/', + ); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/” @override String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名 diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 453a8b2..59fa569 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -258,15 +258,21 @@ class SocialChatUserProfile { _personalPhotos?.add(PersonPhoto.fromJson(v)); }); } - if (json['cpList'] != null) { + final sourceCpList = sourceJson is Map ? sourceJson['cpList'] : null; + final sourceCloseFriendList = + sourceJson is Map ? sourceJson['closeFriendList'] : null; + final cpListJson = json['cpList'] ?? sourceCpList; + final closeFriendListJson = + json['closeFriendList'] ?? sourceCloseFriendList; + if (cpListJson != null) { _cpList = []; - json['cpList'].forEach((v) { + cpListJson.forEach((v) { _cpList?.add(CPRes.fromJson(v)); }); } - if (json['closeFriendList'] != null) { + if (closeFriendListJson != null) { _closeFriendList = []; - json['closeFriendList'].forEach((v) { + closeFriendListJson.forEach((v) { _closeFriendList?.add(CPRes.fromJson(v)); }); } @@ -732,7 +738,11 @@ SocialChatUserProfile? socialChatUserProfileFromJsonWithVipHint( } if (json is Map && json[userProfileKey] != null) { return socialChatProfileWithVipLevelHint( - SocialChatUserProfile.fromJson(json[userProfileKey]), + SocialChatUserProfile.fromJson( + userProfileKey == 'userProfile' + ? json + : {'userProfile': json[userProfileKey]}, + ), json, ); } diff --git a/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart b/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart index f8ab015..0b60918 100644 --- a/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart +++ b/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart @@ -237,8 +237,13 @@ class SCFirstRechargeRewardItem { return amount > 0 ? '${_formatNumber(amount)} Coins' : ''; } return _firstNonEmpty([ - typeName, - _formatRewardTypeName(type), + _inferRewardTypeName( + typeName: typeName, + type: type, + name: name, + content: content, + cover: cover, + ), name, content, quantity > 0 ? 'x$quantity' : '', @@ -455,6 +460,102 @@ String _formatRewardTypeName(String type) { .join(' '); } +String _inferRewardTypeName({ + required String typeName, + required String type, + required String name, + required String content, + required String cover, +}) { + final explicitTypeName = _normalizeSpecificRewardTypeName(typeName); + if (explicitTypeName.isNotEmpty) { + return explicitTypeName; + } + + final searchable = + [ + type, + name, + content, + cover, + ].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase(); + if (_containsAny(searchable, const [ + 'frame', + 'avatar_frame', + 'profile_frame', + 'room_frame', + 'head_frame', + 'border', + '头像框', + '相框', + '框', + ])) { + return 'Frames'; + } + if (_containsAny(searchable, const [ + 'vehicle', + 'vehicles', + 'car', + 'mount', + 'ride', + 'rider', + 'wagon', + '座驾', + '坐骑', + '车辆', + ])) { + return 'Vehicles'; + } + if (_containsAny(searchable, const [ + 'vip', + 'svip', + 'vip_level', + '贵族', + '会员', + ])) { + return 'VIP'; + } + + return _normalizeSpecificRewardTypeName(_formatRewardTypeName(type)); +} + +String _normalizeSpecificRewardTypeName(String value) { + final text = value.trim(); + if (text.isEmpty) { + return ''; + } + final normalized = text.toLowerCase().replaceAll(RegExp(r'[_\-\s]+'), ''); + if (normalized == 'prop' || normalized == 'props' || normalized == '道具') { + return ''; + } + if (normalized.contains('frame') || text.contains('框')) { + return 'Frames'; + } + if (normalized.contains('vehicle') || + normalized.contains('mount') || + normalized.contains('ride') || + text.contains('座驾') || + text.contains('坐骑') || + text.contains('车辆')) { + return 'Vehicles'; + } + if (normalized.contains('vip') || + text.contains('贵族') || + text.contains('会员')) { + return 'VIP'; + } + return text; +} + +bool _containsAny(String value, List tokens) { + for (final token in tokens) { + if (value.contains(token)) { + return true; + } + } + return false; +} + String _formatNumber(num value) { final text = value.toInt().toString(); final buffer = StringBuffer(); diff --git a/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart b/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart index 51f3dbb..e35a07f 100644 --- a/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart +++ b/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart @@ -780,8 +780,8 @@ class _RewardCover extends StatelessWidget { return CachedNetworkImage( imageUrl: cover, fit: BoxFit.contain, - placeholder: (_, __) => _placeholder(), - errorWidget: (_, __, ___) => _placeholder(), + placeholder: (_, __) => _fallbackCover(), + errorWidget: (_, __, ___) => _fallbackCover(), ); } @@ -789,14 +789,14 @@ class _RewardCover extends StatelessWidget { return Image.asset( cover, fit: BoxFit.contain, - errorBuilder: (_, __, ___) => _placeholder(), + errorBuilder: (_, __, ___) => _fallbackCover(), ); } - return _placeholder(); + return _fallbackCover(); } - Widget _placeholder() { + Widget _fallbackCover() { if (reward.isCoinReward) { return Image.asset( _FirstRechargeAssets.yumiCoin, @@ -806,6 +806,11 @@ class _RewardCover extends StatelessWidget { ); } + final fallbackAsset = _fallbackAsset; + if (fallbackAsset.isNotEmpty) { + return Image.asset(fallbackAsset, fit: BoxFit.contain); + } + return Container( width: 48 * scale, height: 48 * scale, @@ -824,6 +829,23 @@ class _RewardCover extends StatelessWidget { ), ); } + + String get _fallbackAsset { + final title = reward.title.trim().toLowerCase(); + if (title.contains('vehicle')) { + return _FirstRechargeAssets.rewardVehicle; + } + if (title.contains('frame')) { + return _FirstRechargeAssets.rewardFrame; + } + if (title == 'vip' || title.contains('vip')) { + return _FirstRechargeAssets.rewardVip; + } + if (title.contains('gift')) { + return _FirstRechargeAssets.rewardGift; + } + return ''; + } } class _DiscountBadge extends StatelessWidget { @@ -960,6 +982,7 @@ class _FirstRechargeAssets { static const String rewardCoinStack = 'sc_images/first_recharge/reward_coin_stack.png'; static const String rewardGift = 'sc_images/first_recharge/reward_gift.png'; + static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png'; static const String buyNowButton = 'sc_images/first_recharge/buy_now_button.png'; static const String discountBadge = diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart index 1d5bfec..20c2f23 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart @@ -472,7 +472,7 @@ class RoomRocketApiMapper { } static List? noteSectionsFromRule(dynamic rule) { - final text = _localizedRuleText(rule); + final text = _normalizeRuleLineBreaks(_localizedRuleText(rule)).trim(); if (text.isEmpty) { return null; } @@ -583,7 +583,11 @@ class RoomRocketApiMapper { if (decodedValue is Map) { return _localizedRuleText(decodedValue); } - return decodedValue.toString().trim(); + return _normalizeRuleLineBreaks(decodedValue.toString()).trim(); + } + + static String _normalizeRuleLineBreaks(String text) { + return text.replaceAll(RegExp(r'\\r\\n|\\n|\\r'), '\n'); } static List _ruleLanguageCandidates(String lang) { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart index 2abaef7..48b7e09 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart @@ -598,7 +598,21 @@ class _RocketRuleHtmlParser { } void _appendText(String text) { - final normalized = text.replaceAll(RegExp(r'[ \t\r\n\f]+'), ' '); + final parts = _normalizeTextNewlines(text).split(RegExp(r'\n+')); + for (var index = 0; index < parts.length; index += 1) { + _appendInlineText(parts[index]); + if (index < parts.length - 1 && + _shouldPreserveTextBreak( + currentPart: parts[index], + remainingParts: parts.skip(index + 1), + )) { + _appendBreak(); + } + } + } + + void _appendInlineText(String text) { + final normalized = text.replaceAll(RegExp(r'[ \t\f]+'), ' '); if (normalized.isEmpty) { return; } @@ -616,6 +630,27 @@ class _RocketRuleHtmlParser { _appendRaw(textAfterMarker, style); } + String _normalizeTextNewlines(String text) { + return text + .replaceAll(RegExp(r'\\r\\n|\\n|\\r'), '\n') + .replaceAll('\r\n', '\n') + .replaceAll('\r', '\n'); + } + + bool _shouldPreserveTextBreak({ + required String currentPart, + required Iterable remainingParts, + }) { + final hasTextBeforeBreak = + currentPart.trim().isNotEmpty || + (_segments.isNotEmpty && !_segments.last.text.endsWith('\n')); + if (!hasTextBeforeBreak) { + return false; + } + return currentPart.trim().isNotEmpty || + remainingParts.any((part) => part.trim().isNotEmpty); + } + void _appendRaw(String text, TextStyle style) { if (text.isEmpty) { return; diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart index 358ce17..f4f3f87 100644 --- a/test/room_rocket_status_test.dart +++ b/test/room_rocket_status_test.dart @@ -42,6 +42,21 @@ void main() { } }); + test('normalizes escaped newline sequences from rocket rule response', () { + final previousLang = SCGlobalConfig.lang; + SCGlobalConfig.lang = 'en'; + try { + final sections = RoomRocketApiMapper.noteSectionsFromRule({ + 'en': '

Line 1\\nLine 2

', + }); + + expect(sections?.single.body, '

Line 1\nLine 2

'); + expect(sections?.single.isHtml, isTrue); + } finally { + SCGlobalConfig.lang = previousLang; + } + }); + test('formats rocket rule html lists without detached markers', () { final text = debugRoomRocketRuleHtmlPlainText( '

How to Launch a Rocket

' @@ -57,6 +72,19 @@ void main() { expect(text, isNot(contains('2. \n'))); }); + test('preserves newline characters inside rocket rule html text', () { + final text = debugRoomRocketRuleHtmlPlainText( + '

1. Sending gifts in the room will add fuel.\n' + '2. Once the rocket fuel is full, it will launch.

', + ); + + expect( + text, + '1. Sending gifts in the room will add fuel.\n' + '2. Once the rocket fuel is full, it will launch.', + ); + }); + test('keeps manually numbered rocket rule html from duplicating markers', () { final text = debugRoomRocketRuleHtmlPlainText( '

How to Launch a Rocket

' diff --git a/test/user_profile_vip_colored_id_test.dart b/test/user_profile_vip_colored_id_test.dart index 84a7a5f..1914179 100644 --- a/test/user_profile_vip_colored_id_test.dart +++ b/test/user_profile_vip_colored_id_test.dart @@ -87,6 +87,25 @@ void main() { expect(card.userProfile?.shouldShowColoredSpecialIdText(), isTrue); }); + test('preserves outer relation lists on room user card profile', () { + final card = RoomUserCardRes.fromJson({ + 'userProfile': {'id': '100', 'account': '10001'}, + 'closeFriendList': [ + {'meUserId': '100', 'cpUserId': '201', 'relationType': 'BROTHER'}, + {'meUserId': '100', 'cpUserId': '202', 'relationType': 'BROTHER'}, + {'meUserId': '100', 'cpUserId': '203', 'relationType': 'BROTHER'}, + {'meUserId': '100', 'cpUserId': '301', 'relationType': 'SISTERS'}, + {'meUserId': '100', 'cpUserId': '302', 'relationType': 'SISTERS'}, + ], + }); + + expect(card.userProfile?.closeFriendList, hasLength(5)); + expect( + card.userProfile?.closeFriendList?.map((item) => item.cpUserId), + containsAll(['201', '202', '203', '301', '302']), + ); + }); + test('pipes room owner userSVipLevel into nested profile', () { final room = SocialChatRoomRes.fromJson({ 'userSVipLevel': '5', diff --git a/需求进度.md b/需求进度.md index e4edf6a..3a807bf 100644 --- a/需求进度.md +++ b/需求进度.md @@ -1038,3 +1038,13 @@ - [ ] 20:39:56 开始格式化并静态检查金币金额显示修正。 - [x] 20:41:16 金币金额显示修正已通过格式化与 flutter analyze 检查。 + +- [ ] 16:40:00 新增识别规则:首充非金币奖励不能泛化显示 Props,需要前端兜底识别 Frames / Vehicles / VIP。 + +- [ ] 16:40:32 开始编码:增加 Frames/Vehicles/VIP 文本识别,并给这些类型补本地图标兜底。 + +- [x] 16:41:04 已增加 Frames/Vehicles/VIP 文本识别与本地图标兜底,避免泛化显示 Props 和黄色占位图。 + +- [ ] 16:41:11 开始格式化并静态检查首充类型识别与兜底图标改动。 + +- [x] 16:41:33 类型识别与兜底图标改动已通过 dart format 和 flutter analyze。