方案1
This commit is contained in:
parent
6e10d57808
commit
ea296e461b
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية.",
|
||||
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
|
||||
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
|
||||
"launchRocket": "إطلاق الصاروخ"
|
||||
"launchRocket": "إطلاق الصاروخ",
|
||||
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।",
|
||||
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
|
||||
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
|
||||
"launchRocket": "রকেট চালু করুন"
|
||||
"launchRocket": "রকেট চালু করুন",
|
||||
"roomRocketFuelFullNotice": "রকেটের জ্বালানি পূর্ণ! {1} সেকেন্ড পরে উৎক্ষেপণ!"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.",
|
||||
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.",
|
||||
"roomRocketRecordEmptyText": "No reward records",
|
||||
"launchRocket": "Launch Rocket"
|
||||
"launchRocket": "Launch Rocket",
|
||||
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. Chaque fois qu'un niveau VIP est acheté, il est valable 30 jours et peut être renouvelé manuellement après expiration.",
|
||||
"roomRocketRewardEmptyText": "Aucune récompense gagnée. Plus de chance la prochaine fois.",
|
||||
"roomRocketRecordEmptyText": "Aucun enregistrement de récompense",
|
||||
"launchRocket": "Lancement de fusée"
|
||||
"launchRocket": "Lancement de fusée",
|
||||
"roomRocketFuelFullNotice": "Le carburant de la fusée est plein ! Lancement dans {1} s !"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. Ogni volta che viene acquistato un livello VIP, è valido per 30 giorni e può essere rinnovato manualmente dopo la scadenza.",
|
||||
"roomRocketRewardEmptyText": "Nessun premio guadagnato. Più fortuna la prossima volta.",
|
||||
"roomRocketRecordEmptyText": "Nessun record di ricompensa",
|
||||
"launchRocket": "Lancia il razzo"
|
||||
"launchRocket": "Lancia il razzo",
|
||||
"roomRocketFuelFullNotice": "Carburante del razzo pieno! Lancio tra {1} s!"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. Cada vez que um nível VIP é adquirido, ele é válido por 30 dias, podendo ser renovado manualmente após expirar.",
|
||||
"roomRocketRewardEmptyText": "Nenhuma recompensa ganha. Melhor sorte da próxima vez.",
|
||||
"roomRocketRecordEmptyText": "Nenhum registro de recompensa",
|
||||
"launchRocket": "Lançar foguete"
|
||||
"launchRocket": "Lançar foguete",
|
||||
"roomRocketFuelFullNotice": "Combustível do foguete cheio! Lançamento em {1}s!"
|
||||
}
|
||||
|
||||
@ -726,5 +726,6 @@
|
||||
"roomRocketNoteSectionBody": "1. Her VIP seviyesi satın alındığında 30 gün geçerlidir ve süresi dolduktan sonra manuel olarak yenilenebilir.",
|
||||
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
|
||||
"roomRocketRecordEmptyText": "Ödül kaydı yok",
|
||||
"launchRocket": "Roketi Fırlat"
|
||||
"launchRocket": "Roketi Fırlat",
|
||||
"roomRocketFuelFullNotice": "Roket yakıtı doldu! {1} saniye sonra fırlatılıyor!"
|
||||
}
|
||||
|
||||
@ -79,6 +79,9 @@ class SCRoomMsgType {
|
||||
///用户火箭中奖
|
||||
static const String rocketRewardUser = "ROCKET_REWARD_USER";
|
||||
|
||||
///火箭燃料满倒计时发射提示
|
||||
static const String rocketLaunchNotice = "ROCKET_LAUNCH_NOTICE";
|
||||
|
||||
///语音房火箭状态更新
|
||||
static const String voiceRoomRocketStatusUpdate =
|
||||
"VOICE_ROOM_ROCKET_STATUS_UPDATE";
|
||||
|
||||
@ -845,6 +845,9 @@ class SCAppLocalizations {
|
||||
|
||||
String get launchRocket => translate('launchRocket');
|
||||
|
||||
String roomRocketFuelFullNotice(String seconds) =>
|
||||
translate('roomRocketFuelFullNotice').replaceAll('{1}', seconds);
|
||||
|
||||
String get useCoupontips => translate('useCoupontips');
|
||||
|
||||
String get joinMemberTips2 => translate('joinMemberTips2');
|
||||
|
||||
@ -54,7 +54,6 @@ import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push
|
||||
hide Data;
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart';
|
||||
@ -119,7 +118,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
|
||||
|
||||
BuildContext? context;
|
||||
final Set<String> _sentRoomRocketRewardMessageKeys = <String>{};
|
||||
|
||||
void _giftFxLog(String message) {}
|
||||
|
||||
@ -1106,6 +1104,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return true;
|
||||
case SCRoomMsgType.rocketRewardUser:
|
||||
return true;
|
||||
case SCRoomMsgType.rocketLaunchNotice:
|
||||
return true;
|
||||
case SCRoomMsgType.emoticons:
|
||||
return (msg.number ?? -1) < 0;
|
||||
default:
|
||||
@ -1139,6 +1139,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hasRoomRocketRewardMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.rocketLaunchNotice &&
|
||||
_hasRoomRocketNoticeMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
roomAllMsgList.insert(0, msg);
|
||||
if (roomAllMsgList.length > 250) {
|
||||
@ -1177,6 +1181,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.rocketLaunchNotice) {
|
||||
roomChatMsgList.insert(0, msg);
|
||||
if (roomChatMsgList.length > 250) {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.gift ||
|
||||
msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
roomGiftMsgList.insert(0, msg);
|
||||
@ -1296,6 +1306,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasRoomRocketNoticeMessage(Msg msg) {
|
||||
final key = (msg.giftBatchId ?? '').trim();
|
||||
if (key.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return roomAllMsgList.any(
|
||||
(item) =>
|
||||
item.type == SCRoomMsgType.rocketLaunchNotice &&
|
||||
(item.giftBatchId ?? '').trim() == key,
|
||||
);
|
||||
}
|
||||
|
||||
String _roomRocketRewardMessageKey(Msg msg) {
|
||||
final stableKey = (msg.giftBatchId ?? '').trim();
|
||||
if (stableKey.isNotEmpty) {
|
||||
@ -1891,6 +1913,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
rtcProvider.shouldShowRoomVisualEffects &&
|
||||
rtcProvider.isVoiceRoomRouteVisible;
|
||||
if (isCurrentVisibleRoom) {
|
||||
addMsg(
|
||||
_buildRoomRocketLaunchNoticeMsg(
|
||||
_firstNonBlank([
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||||
launch.roomAccount,
|
||||
]),
|
||||
launch,
|
||||
),
|
||||
);
|
||||
rtcProvider.handleRoomRocketLaunchBroadcast(launch);
|
||||
}
|
||||
}
|
||||
@ -1924,260 +1955,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
unawaited(_showRoomRocketRewardPopupDialog(resolvedRoomId));
|
||||
}
|
||||
|
||||
Future<void> _sendRoomRocketRewardPublicMessages(
|
||||
String roomId,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final groupId = _currentVoiceRoomGroupId();
|
||||
if (groupId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final launchNo = _payloadText(payload['launchNo']);
|
||||
var records = _rocketRewardRecordsFromPayload(payload);
|
||||
if (records.isEmpty) {
|
||||
records = await _loadRocketRewardPublicRecords(roomId, launchNo);
|
||||
}
|
||||
if (records.isNotEmpty) {
|
||||
for (final record in records) {
|
||||
await _sendRoomRocketRewardMessage(
|
||||
groupId,
|
||||
_buildRocketRewardRecordMsg(groupId, record),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final userIds = _payloadStringList(payload['userIds']);
|
||||
for (final userId in userIds) {
|
||||
await _sendRoomRocketRewardMessage(
|
||||
groupId,
|
||||
_buildRocketRewardFallbackMsg(groupId, payload, userId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendRoomRocketRewardMessage(String groupId, Msg msg) async {
|
||||
final key = _roomRocketRewardMessageKey(msg);
|
||||
if (key.isEmpty || !_sentRoomRocketRewardMessageKeys.add(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final textMsg = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.createCustomMessage(data: jsonEncode(msg.toJson()));
|
||||
if (textMsg.code != 0 || textMsg.data?.id == null) {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
return;
|
||||
}
|
||||
final sendResult = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: '');
|
||||
if (sendResult.code == 0) {
|
||||
addMsg(msg);
|
||||
} else {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
}
|
||||
} catch (_) {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SCRoomRocketRewardRecordRes>> _loadRocketRewardPublicRecords(
|
||||
String roomId,
|
||||
String launchNo,
|
||||
) async {
|
||||
if (roomId.trim().isEmpty || launchNo.trim().isEmpty) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
roomId,
|
||||
cursor: 1,
|
||||
limit: 100,
|
||||
);
|
||||
return res.records
|
||||
.where((item) => item.launchNo.trim() == launchNo.trim())
|
||||
.toList(growable: false);
|
||||
} catch (_) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
}
|
||||
|
||||
List<SCRoomRocketRewardRecordRes> _rocketRewardRecordsFromPayload(
|
||||
Map<String, dynamic> payload,
|
||||
) {
|
||||
final candidates = [
|
||||
payload['records'],
|
||||
payload['rewardRecords'],
|
||||
payload['rewards'],
|
||||
_broadcastPayloadMap(payload['body'])['records'],
|
||||
_broadcastPayloadMap(payload['data'])['records'],
|
||||
];
|
||||
for (final candidate in candidates) {
|
||||
if (candidate is Iterable) {
|
||||
final records = candidate
|
||||
.map(SCRoomRocketRewardRecordRes.fromJson)
|
||||
.toList(growable: false);
|
||||
final usableRecords = records
|
||||
.where((item) => item.userId.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (usableRecords.isNotEmpty) {
|
||||
return usableRecords;
|
||||
}
|
||||
}
|
||||
}
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
|
||||
Msg _buildRocketRewardRecordMsg(
|
||||
String groupId,
|
||||
SCRoomRocketRewardRecordRes record,
|
||||
) {
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: record.rewardCover,
|
||||
type: SCRoomMsgType.rocketRewardUser,
|
||||
user: SocialChatUserProfile(
|
||||
id: record.userId,
|
||||
userNickname: record.displayName,
|
||||
userAvatar: record.userAvatar,
|
||||
account: record.account,
|
||||
),
|
||||
role: _rocketRewardRole(record.rewardType, record.rewardCover),
|
||||
number: record.level <= 0 ? 1 : record.level,
|
||||
rocketIconUrl: _firstNonBlank([
|
||||
record.rocketIconUrl,
|
||||
_rocketIconUrlForLevel(record.level),
|
||||
]),
|
||||
giftBatchId: _rocketRewardRecordKey(record),
|
||||
);
|
||||
}
|
||||
|
||||
Msg _buildRocketRewardFallbackMsg(
|
||||
String groupId,
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final rewardCover = _firstNonBlank([
|
||||
_payloadText(payload['rewardCover']),
|
||||
_payloadText(payload['rewardIcon']),
|
||||
_payloadText(payload['rewardUrl']),
|
||||
_payloadText(payload['giftUrl']),
|
||||
_payloadText(payload['giftIcon']),
|
||||
]);
|
||||
final rewardType = _firstNonBlank([
|
||||
_payloadText(payload['rewardType']),
|
||||
_payloadText(payload['type']),
|
||||
rewardCover.isEmpty ? 'GOLD' : '',
|
||||
]);
|
||||
final level = (_payloadNum(payload['level']) ?? 1).toInt();
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: rewardCover,
|
||||
type: SCRoomMsgType.rocketRewardUser,
|
||||
user: _rocketRewardUserFromPayload(payload, userId),
|
||||
role: _rocketRewardRole(rewardType, rewardCover),
|
||||
number: level <= 0 ? 1 : level,
|
||||
rocketIconUrl: _rocketRewardRocketIconUrl(payload, level),
|
||||
giftBatchId: [
|
||||
'rocket_reward_fallback',
|
||||
_payloadText(payload['launchNo']),
|
||||
userId,
|
||||
level,
|
||||
rewardType,
|
||||
].join('|'),
|
||||
);
|
||||
}
|
||||
|
||||
SocialChatUserProfile _rocketRewardUserFromPayload(
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final userPayload = _rocketRewardUserPayload(payload, userId);
|
||||
final nickname = _firstNonBlank([
|
||||
_payloadText(userPayload['userNickname']),
|
||||
_payloadText(userPayload['nickname']),
|
||||
_payloadText(userPayload['nickName']),
|
||||
_payloadText(userPayload['account']),
|
||||
userId,
|
||||
]);
|
||||
return SocialChatUserProfile(
|
||||
id: _firstNonBlank([_payloadText(userPayload['userId']), userId]),
|
||||
userNickname: nickname,
|
||||
userAvatar: _firstNonBlank([
|
||||
_payloadText(userPayload['userAvatar']),
|
||||
_payloadText(userPayload['avatar']),
|
||||
_payloadText(userPayload['avatarUrl']),
|
||||
]),
|
||||
account: _payloadText(userPayload['account']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _rocketRewardUserPayload(
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final candidates = [
|
||||
payload['users'],
|
||||
payload['rewardUsers'],
|
||||
payload['winnerUsers'],
|
||||
payload['winners'],
|
||||
];
|
||||
for (final candidate in candidates) {
|
||||
if (candidate is Map) {
|
||||
final item = candidate[userId] ?? candidate[userId.toString()];
|
||||
final map = _broadcastPayloadMap(item);
|
||||
if (map.isNotEmpty) {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
if (candidate is Iterable) {
|
||||
for (final item in candidate) {
|
||||
final map = _broadcastPayloadMap(item);
|
||||
final id = _firstNonBlank([
|
||||
_payloadText(map['userId']),
|
||||
_payloadText(map['id']),
|
||||
_payloadText(map['account']),
|
||||
]);
|
||||
if (id == userId) {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return <String, dynamic>{'userId': userId, 'userNickname': userId};
|
||||
}
|
||||
|
||||
String _rocketRewardRole(String rewardType, String rewardCover) {
|
||||
final normalized = rewardType.trim().toUpperCase();
|
||||
if (normalized == 'GOLD' || normalized == 'COIN' || normalized == 'COINS') {
|
||||
return 'GOLD';
|
||||
}
|
||||
return rewardCover.trim().isEmpty ? 'GOLD' : normalized;
|
||||
}
|
||||
|
||||
String _rocketRewardRocketIconUrl(Map<String, dynamic> payload, int level) {
|
||||
final rocket = _broadcastPayloadMap(payload['rocket']);
|
||||
final levelInfo = _broadcastPayloadMap(payload['levelInfo']);
|
||||
final config = _broadcastPayloadMap(payload['config']);
|
||||
return _firstNonBlank([
|
||||
_payloadText(payload['rocket_icon_url']),
|
||||
_payloadText(payload['rocketIconUrl']),
|
||||
_payloadText(payload['rocketIcon']),
|
||||
_payloadText(payload['iconUrl']),
|
||||
_payloadText(rocket['rocket_icon_url']),
|
||||
_payloadText(rocket['rocketIconUrl']),
|
||||
_payloadText(rocket['rocketIcon']),
|
||||
_payloadText(levelInfo['rocket_icon_url']),
|
||||
_payloadText(levelInfo['rocketIconUrl']),
|
||||
_payloadText(levelInfo['rocketIcon']),
|
||||
_payloadText(config['rocket_icon_url']),
|
||||
_payloadText(config['rocketIconUrl']),
|
||||
_payloadText(config['rocketIcon']),
|
||||
_rocketIconUrlForLevel(level),
|
||||
]);
|
||||
}
|
||||
|
||||
String _rocketIconUrlForLevel(int level) {
|
||||
final currentContext = context;
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
@ -2196,29 +1973,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return '';
|
||||
}
|
||||
|
||||
String _rocketRewardRecordKey(SCRoomRocketRewardRecordRes record) {
|
||||
return [
|
||||
'rocket_reward',
|
||||
record.id,
|
||||
record.launchNo,
|
||||
record.userId,
|
||||
record.rewardScene,
|
||||
record.rewardType,
|
||||
record.rewardItemId,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
Future<void> _showRoomRocketRewardPopupDialog(String roomId) async {
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
|
||||
if (res.records.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
_sendRoomRocketRewardPublicMessages(roomId, {
|
||||
'records': res.records.map((item) => item.toJson()).toList(),
|
||||
}),
|
||||
);
|
||||
final recordIds = res.records
|
||||
.map((item) => item.id)
|
||||
.where((item) => item.isNotEmpty)
|
||||
@ -2273,6 +2033,29 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Msg _buildRoomRocketLaunchNoticeMsg(
|
||||
String groupId,
|
||||
RoomRocketLaunchBroadcastMessage launch,
|
||||
) {
|
||||
final seconds = launch.durationSeconds <= 0 ? 10 : launch.durationSeconds;
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: seconds.toString(),
|
||||
type: SCRoomMsgType.rocketLaunchNotice,
|
||||
number: seconds,
|
||||
rocketIconUrl: _firstNonBlank([
|
||||
launch.rocketIconUrl,
|
||||
_rocketIconUrlForLevel(launch.safeLevel),
|
||||
]),
|
||||
giftBatchId: [
|
||||
'rocket_launch_notice',
|
||||
launch.launchNo,
|
||||
launch.roomId,
|
||||
launch.safeLevel,
|
||||
].join('|'),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _payloadStringList(dynamic value) {
|
||||
if (value is Iterable) {
|
||||
return value
|
||||
|
||||
@ -376,8 +376,8 @@ class SCRoomRocketRewardRes {
|
||||
_asMap(data['config'])['rocketIconUrl'],
|
||||
_asMap(data['config'])['rocketIcon'],
|
||||
]),
|
||||
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
|
||||
expireDays: _intValue(data['expireDays']),
|
||||
rewardAmount: _rewardAmountValue(data['rewardAmount']) ?? 0,
|
||||
expireDays: _dayValue(data['expireDays']),
|
||||
);
|
||||
}
|
||||
|
||||
@ -887,7 +887,7 @@ class SCRoomRocketRewardRecordRes {
|
||||
rewardItemId: _stringValue(data['rewardItemId']),
|
||||
rewardName: _stringValue(data['rewardName']),
|
||||
rewardCover: _stringValue(data['rewardCover']),
|
||||
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
|
||||
rewardAmount: _rewardAmountValue(data['rewardAmount']) ?? 0,
|
||||
rocketIconUrl: _firstAssetString([
|
||||
data['rocket_icon_url'],
|
||||
data['rocketIconUrl'],
|
||||
@ -903,7 +903,7 @@ class SCRoomRocketRewardRecordRes {
|
||||
_asMap(data['config'])['rocketIconUrl'],
|
||||
_asMap(data['config'])['rocketIcon'],
|
||||
]),
|
||||
expireDays: _intValue(data['expireDays']),
|
||||
expireDays: _dayValue(data['expireDays']),
|
||||
grantStatus: _stringValue(data['grantStatus']),
|
||||
popupStatus: _stringValue(data['popupStatus']),
|
||||
grantTime: _stringValue(data['grantTime']),
|
||||
@ -1116,6 +1116,24 @@ num? _numValue(dynamic value) {
|
||||
return num.tryParse(_stringValue(value));
|
||||
}
|
||||
|
||||
num? _rewardAmountValue(dynamic value) {
|
||||
return _numValue(value) ?? _dayNumberValue(value);
|
||||
}
|
||||
|
||||
int _dayValue(dynamic value, {int fallback = 0}) {
|
||||
final numeric = _numValue(value) ?? _dayNumberValue(value);
|
||||
return numeric?.toInt() ?? fallback;
|
||||
}
|
||||
|
||||
num? _dayNumberValue(dynamic value) {
|
||||
final text = _stringValue(value);
|
||||
final match = RegExp(
|
||||
r'^(\d+(?:\.\d+)?)\s*d$',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
return match == null ? null : num.tryParse(match.group(1)!);
|
||||
}
|
||||
|
||||
bool _boolValue(dynamic value, {bool fallback = false}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
|
||||
@ -25,7 +25,17 @@ class RoomRocketLaunchBroadcastMessage {
|
||||
level: _intValue(data['level'], fallback: 1),
|
||||
rocketIconUrl: _rocketIconUrl(data),
|
||||
rocketAnimationUrl: _rocketAnimationUrl(data),
|
||||
durationSeconds: _intValue(data['durationSeconds'], fallback: 7),
|
||||
durationSeconds: _firstIntValue([
|
||||
data['countdown_seconds'],
|
||||
data['countdownSeconds'],
|
||||
data['launch_delay_seconds'],
|
||||
data['launchDelaySeconds'],
|
||||
data['delaySeconds'],
|
||||
data['durationSeconds'],
|
||||
data['broadcastDurationSeconds'],
|
||||
_asMap(data['config'])['countdownSeconds'],
|
||||
_asMap(data['config'])['launchDelaySeconds'],
|
||||
], fallback: 10),
|
||||
triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']),
|
||||
triggerUserId: _stringValue(data['triggerUserId']),
|
||||
launchNo: _stringValue(data['launchNo']),
|
||||
@ -59,7 +69,7 @@ class RoomRocketLaunchBroadcastMessage {
|
||||
|
||||
Duration get displayDuration {
|
||||
if (durationSeconds <= 0) {
|
||||
return const Duration(seconds: 7);
|
||||
return const Duration(seconds: 10);
|
||||
}
|
||||
return Duration(seconds: durationSeconds);
|
||||
}
|
||||
@ -195,3 +205,18 @@ int _intValue(dynamic value, {int fallback = 0}) {
|
||||
}
|
||||
return int.tryParse(_stringValue(value)) ?? fallback;
|
||||
}
|
||||
|
||||
int _firstIntValue(Iterable<dynamic> values, {int fallback = 0}) {
|
||||
for (final value in values) {
|
||||
final parsed =
|
||||
value is int
|
||||
? value
|
||||
: value is num
|
||||
? value.toInt()
|
||||
: int.tryParse(_stringValue(value));
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@ -235,7 +235,28 @@ class RoomRocketRewardItem {
|
||||
if (isGoldReward) {
|
||||
return amount;
|
||||
}
|
||||
return expireDays > 0 ? '${expireDays}D' : amount;
|
||||
return _formatDayText(expireDays > 0 ? expireDays.toString() : amount);
|
||||
}
|
||||
|
||||
static String _formatDayText(String value) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) {
|
||||
return text;
|
||||
}
|
||||
final dayMatch = RegExp(
|
||||
r'^(\d+(?:\.\d+)?)\s*d$',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
final dayValue = dayMatch?.group(1) ?? _normalizedNumber(text);
|
||||
return dayValue == null ? text : '${dayValue}d';
|
||||
}
|
||||
|
||||
static String? _normalizedNumber(String value) {
|
||||
final parsed = num.tryParse(value);
|
||||
if (parsed == null) {
|
||||
return null;
|
||||
}
|
||||
return parsed % 1 == 0 ? parsed.toInt().toString() : parsed.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,18 +401,20 @@ class _RocketCanvas extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildRocketBody() {
|
||||
final previewImageUrl = selectedLevelItem.previewImageUrl?.trim() ?? '';
|
||||
return Positioned(
|
||||
left: s(88),
|
||||
top: s(111),
|
||||
width: s(200),
|
||||
height: s(432),
|
||||
child: _RocketImage(
|
||||
asset: selectedLevelItem.asset ?? _RocketAssets.rocketBody,
|
||||
imageUrl: selectedLevelItem.previewImageUrl,
|
||||
imageUrl: previewImageUrl.isEmpty ? null : previewImageUrl,
|
||||
fit: BoxFit.contain,
|
||||
preferSvg: true,
|
||||
pagDisplayScale: 2,
|
||||
pagClipBehavior: Clip.none,
|
||||
allowStaticFallback: false,
|
||||
emptyBuilder: (_) => const SCRotatingDotsLoading(),
|
||||
debugName:
|
||||
'centerPreview level=${selectedLevelItem.level} '
|
||||
'preview=${selectedLevelItem.previewImageUrl ?? '-'}',
|
||||
@ -470,11 +493,6 @@ class _RocketCanvas extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildProgress() {
|
||||
final asset = switch (progressStage) {
|
||||
RoomRocketProgressStage.empty => _RocketAssets.progressEmpty,
|
||||
RoomRocketProgressStage.charging => _RocketAssets.progressPartial,
|
||||
RoomRocketProgressStage.full => _RocketAssets.progressFull,
|
||||
};
|
||||
final progressImageUrl = selectedLevelItem.progressImageUrl?.trim() ?? '';
|
||||
return Stack(
|
||||
children: [
|
||||
@ -483,10 +501,10 @@ class _RocketCanvas extends StatelessWidget {
|
||||
top: s(227),
|
||||
width: s(44),
|
||||
height: s(134),
|
||||
child: _RocketImage(
|
||||
asset: asset,
|
||||
child: _RocketProgressBar(
|
||||
percent: percent,
|
||||
progressStage: progressStage,
|
||||
imageUrl: progressImageUrl.isEmpty ? null : progressImageUrl,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@ -807,6 +825,96 @@ class _RocketCanvas extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketProgressBar extends StatelessWidget {
|
||||
const _RocketProgressBar({
|
||||
required this.percent,
|
||||
required this.progressStage,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
final double percent;
|
||||
final RoomRocketProgressStage progressStage;
|
||||
final String? imageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final height = constraints.maxHeight;
|
||||
final ratio = _progressRatio();
|
||||
final fillHeight = height * ratio;
|
||||
final showFillCap =
|
||||
fillHeight > 0 &&
|
||||
ratio < 1 &&
|
||||
progressStage != RoomRocketProgressStage.full;
|
||||
final capHeight = math.max(1.0, height * 0.014);
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const _RocketImage(
|
||||
asset: _RocketAssets.progressEmpty,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
if (fillHeight > 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: width,
|
||||
height: fillHeight,
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: _RocketImage(
|
||||
asset: _RocketAssets.progressFull,
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showFillCap)
|
||||
Positioned(
|
||||
left: width * 0.08,
|
||||
bottom: math.max(0, fillHeight - capHeight / 2),
|
||||
width: width * 0.84,
|
||||
height: capHeight,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(capHeight),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF30FFC0).withValues(alpha: 0),
|
||||
const Color(0xFFA4FFE3).withValues(alpha: 0.9),
|
||||
const Color(0xFF30FFC0).withValues(alpha: 0),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF2DFFC1).withValues(alpha: 0.7),
|
||||
blurRadius: capHeight * 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _progressRatio() {
|
||||
final value = percent.isFinite ? percent : 0.0;
|
||||
return (value / 100).clamp(0.0, 1.0).toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardCard extends StatelessWidget {
|
||||
const _RewardCard({required this.scale, required this.item, this.onTap});
|
||||
|
||||
@ -978,6 +1086,8 @@ class _RocketImage extends StatelessWidget {
|
||||
this.preferSvg = false,
|
||||
this.pagDisplayScale = 1,
|
||||
this.pagClipBehavior = Clip.hardEdge,
|
||||
this.allowStaticFallback = true,
|
||||
this.emptyBuilder,
|
||||
this.debugName,
|
||||
});
|
||||
|
||||
@ -987,6 +1097,8 @@ class _RocketImage extends StatelessWidget {
|
||||
final bool preferSvg;
|
||||
final double pagDisplayScale;
|
||||
final Clip pagClipBehavior;
|
||||
final bool allowStaticFallback;
|
||||
final WidgetBuilder? emptyBuilder;
|
||||
final String? debugName;
|
||||
|
||||
@override
|
||||
@ -1035,14 +1147,23 @@ class _RocketImage extends StatelessWidget {
|
||||
errorBuilder: (_, error, __) {
|
||||
_debugImage('network svg failed: $error');
|
||||
return preferSvg && !_isSvg(source)
|
||||
? CustomCachedImage(imageUrl: source, fit: fit)
|
||||
? CustomCachedImage(
|
||||
imageUrl: source,
|
||||
fit: fit,
|
||||
errorWidget:
|
||||
allowStaticFallback ? null : const SizedBox.shrink(),
|
||||
)
|
||||
: _imageFallback();
|
||||
},
|
||||
);
|
||||
}
|
||||
return CustomCachedImage(imageUrl: source, fit: fit);
|
||||
return CustomCachedImage(
|
||||
imageUrl: source,
|
||||
fit: fit,
|
||||
errorWidget: allowStaticFallback ? null : const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
if (asset != null && asset!.trim().isNotEmpty) {
|
||||
if (allowStaticFallback && asset != null && asset!.trim().isNotEmpty) {
|
||||
final source = asset!.trim();
|
||||
_debugImage('load asset ${_shortSource(source)}');
|
||||
if (SCNetworkSvgaWidget.isSvga(source)) {
|
||||
@ -1080,7 +1201,7 @@ class _RocketImage extends StatelessWidget {
|
||||
}
|
||||
return Image.asset(source, fit: fit, gaplessPlayback: true);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
return emptyBuilder?.call(context) ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
static bool _isSvg(String value) {
|
||||
@ -1123,6 +1244,9 @@ class _RocketImage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _imageFallback() {
|
||||
if (!allowStaticFallback) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (asset != null &&
|
||||
asset!.trim().isNotEmpty &&
|
||||
!_isSvg(asset!) &&
|
||||
@ -1747,8 +1871,6 @@ class _RocketAssets {
|
||||
'sc_images/room/rocket/bottom_avatar_ring.png';
|
||||
static const String pillBg = 'sc_images/room/rocket/pill_bg.png';
|
||||
static const String progressFull = 'sc_images/room/rocket/progress_full.png';
|
||||
static const String progressPartial =
|
||||
'sc_images/room/rocket/progress_partial.png';
|
||||
static const String progressEmpty =
|
||||
'sc_images/room/rocket/progress_empty.png';
|
||||
static const String rewardTop = 'sc_images/room/rocket/reward_dialog_top.png';
|
||||
|
||||
@ -469,15 +469,6 @@ class _RewardDialogCanvas extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool _handleHorizontalScrollNotification(ScrollNotification notification) {
|
||||
if (notification.metrics.axis != Axis.horizontal) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMore || isLoadingMore) {
|
||||
return false;
|
||||
}
|
||||
if (notification.metrics.extentAfter <= s(96)) {
|
||||
onLoadMore?.call();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -70,10 +70,12 @@ class _RoomRocketRewardDialogLoaderState
|
||||
widget.initialPopupRecords.isNotEmpty &&
|
||||
!_usedInitialPopupRecords;
|
||||
final nextPopupRecords =
|
||||
useInitialPopupRecords
|
||||
? widget.initialPopupRecords
|
||||
: await _loadPopupRecords(requestCursor);
|
||||
if (useInitialPopupRecords) {
|
||||
reset
|
||||
? useInitialPopupRecords
|
||||
? widget.initialPopupRecords
|
||||
: await _loadPopupRecords(requestCursor)
|
||||
: const <SCRoomRocketRewardRecordRes>[];
|
||||
if (reset && useInitialPopupRecords) {
|
||||
_usedInitialPopupRecords = true;
|
||||
}
|
||||
final nextRoomRecords = await _loadRoomRewardRecords(requestCursor);
|
||||
@ -89,16 +91,13 @@ class _RoomRocketRewardDialogLoaderState
|
||||
..clear()
|
||||
..addAll(nextRoomRecords);
|
||||
} else {
|
||||
_popupRecords.addAll(nextPopupRecords);
|
||||
_roomRecords.addAll(nextRoomRecords);
|
||||
}
|
||||
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
|
||||
_winners = RoomRocketApiMapper.winnersFromRecords(
|
||||
_visibleRoomRewardRecords(),
|
||||
);
|
||||
_hasMore =
|
||||
nextPopupRecords.length >= _rewardDialogPageSize ||
|
||||
nextRoomRecords.length >= _rewardDialogPageSize;
|
||||
_hasMore = nextRoomRecords.length >= _rewardDialogPageSize;
|
||||
if (_hasMore) {
|
||||
_cursor = requestCursor + 1;
|
||||
}
|
||||
|
||||
@ -85,6 +85,11 @@ class _MsgItemState extends State<MsgItem> {
|
||||
return _buildRocketRewardMsg(context);
|
||||
}
|
||||
|
||||
///火箭燃料加满倒计时
|
||||
if (widget.msg.type == SCRoomMsgType.rocketLaunchNotice) {
|
||||
return _buildRocketLaunchNoticeMsg(context);
|
||||
}
|
||||
|
||||
///幸运礼物
|
||||
if (widget.msg.type == SCRoomMsgType.gameLuckyGift) {
|
||||
return _buildGameLuckyGiftMsg(context);
|
||||
@ -764,6 +769,73 @@ class _MsgItemState extends State<MsgItem> {
|
||||
return netImage(url: iconUrl, width: 18.w);
|
||||
}
|
||||
|
||||
Widget _buildRocketLaunchNoticeMsg(BuildContext context) {
|
||||
final seconds =
|
||||
(widget.msg.number ?? num.tryParse(widget.msg.msg ?? '') ?? 10).toInt();
|
||||
final template =
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)?.roomRocketFuelFullNotice(seconds.toString()) ??
|
||||
'Rocket fuel is full! Launching in ${seconds}s!';
|
||||
final iconUrl = widget.msg.rocketIconUrl?.trim() ?? '';
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: 42.w, maxWidth: 304.w),
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: 12.w,
|
||||
end: 10.w,
|
||||
top: 8.w,
|
||||
bottom: 8.w,
|
||||
),
|
||||
margin: EdgeInsetsDirectional.only(start: 10.w, end: 36.w, bottom: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F2FF).withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(9.w),
|
||||
border: Border.all(color: const Color(0xFF6672FF), width: 1.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
template,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF6672FF),
|
||||
fontSize: sp(13),
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
SizedBox(
|
||||
width: 42.w,
|
||||
height: 28.w,
|
||||
child:
|
||||
iconUrl.isEmpty
|
||||
? Icon(
|
||||
Icons.rocket_launch_rounded,
|
||||
color: const Color(0xFFB8BEFA),
|
||||
size: 28.w,
|
||||
)
|
||||
: netImage(
|
||||
url: iconUrl,
|
||||
width: 42.w,
|
||||
height: 28.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///幸运礼物
|
||||
_buildGameLuckyGiftMsg(BuildContext context) {
|
||||
List<InlineSpan> spans = [];
|
||||
|
||||
@ -139,6 +139,42 @@ void main() {
|
||||
expect(groups[2]?.inRoom.single.expireDays, 7);
|
||||
});
|
||||
|
||||
test('normalizes non-gold rocket reward days display text', () {
|
||||
final rewards = [
|
||||
SCRoomRocketRewardRes.fromJson({
|
||||
'rewardType': 'PROPS',
|
||||
'rewardAmount': '7d',
|
||||
}),
|
||||
SCRoomRocketRewardRes.fromJson({
|
||||
'rewardType': 'PROPS',
|
||||
'rewardAmount': 1,
|
||||
}),
|
||||
SCRoomRocketRewardRes.fromJson({
|
||||
'rewardType': 'PROPS',
|
||||
'rewardAmount': 1,
|
||||
'expireDays': '30D',
|
||||
}),
|
||||
SCRoomRocketRewardRes.fromJson({
|
||||
'rewardType': 'GOLD',
|
||||
'rewardAmount': 100,
|
||||
}),
|
||||
].map(RoomRocketApiMapper.rewardItemFromPreview).toList(growable: false);
|
||||
|
||||
expect(rewards[0].displayText, '7d');
|
||||
expect(rewards[1].displayText, '1d');
|
||||
expect(rewards[2].displayText, '30d');
|
||||
expect(rewards[3].displayText, '100');
|
||||
|
||||
final recordRewards = RoomRocketApiMapper.rewardItemsFromRecords([
|
||||
SCRoomRocketRewardRecordRes.fromJson({
|
||||
'rewardType': 'PROPS',
|
||||
'rewardAmount': '7d',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(recordRewards.single.displayText, '7d');
|
||||
});
|
||||
|
||||
test('parses rocket level contents response from go api', () {
|
||||
final contents = SCRoomRocketLevelContentsRes.fromJson({
|
||||
'configured': true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user