bug
This commit is contained in:
parent
85a1b01347
commit
35c05a9123
@ -577,6 +577,8 @@
|
||||
"resetsDailyAtMidnight": "يُعاد التعيين يوميًا عند منتصف الليل",
|
||||
"limitedToOneTime": "لمرة واحدة فقط",
|
||||
"useMicrophoneForOneMin": "استخدم الميكروفون لمدة دقيقة واحدة",
|
||||
"taskRequirement": "المتطلبات",
|
||||
"taskProgress": "التقدم",
|
||||
"rewardClaimedSuccessfully": "تم استلام المكافأة بنجاح",
|
||||
"rewardClaimFailed": "فشل استلام المكافأة",
|
||||
"nickName": "الاسم المستعار",
|
||||
|
||||
@ -513,6 +513,8 @@
|
||||
"resetsDailyAtMidnight": "প্রতিদিন মধ্যরাতে রিসেট হয়",
|
||||
"limitedToOneTime": "শুধু একবারের জন্য",
|
||||
"useMicrophoneForOneMin": "১ মিনিট মাইক্রোফোন ব্যবহার করুন",
|
||||
"taskRequirement": "প্রয়োজন",
|
||||
"taskProgress": "অগ্রগতি",
|
||||
"rewardClaimedSuccessfully": "পুরস্কার সফলভাবে গ্রহণ করা হয়েছে",
|
||||
"rewardClaimFailed": "পুরস্কার গ্রহণ ব্যর্থ হয়েছে",
|
||||
"enterRoomConfirmTips": "আপনি কি রুমে যেতে চান?",
|
||||
|
||||
@ -483,6 +483,8 @@
|
||||
"resetsDailyAtMidnight": "Resets daily at midnight",
|
||||
"limitedToOneTime": "Limited to one time",
|
||||
"useMicrophoneForOneMin": "Use the microphone for 1 min",
|
||||
"taskRequirement": "Requirement",
|
||||
"taskProgress": "Progress",
|
||||
"rewardClaimedSuccessfully": "Reward claimed successfully",
|
||||
"rewardClaimFailed": "Reward claim failed",
|
||||
"enterRoomConfirmTips": "Are you sure you want to enter the room?",
|
||||
|
||||
@ -482,6 +482,8 @@
|
||||
"resetsDailyAtMidnight": "Her gün gece yarısı sıfırlanır",
|
||||
"limitedToOneTime": "Yalnızca bir kez",
|
||||
"useMicrophoneForOneMin": "Mikrofonu 1 dakika kullan",
|
||||
"taskRequirement": "Gereksinim",
|
||||
"taskProgress": "İlerleme",
|
||||
"rewardClaimedSuccessfully": "Ödül başarıyla alındı",
|
||||
"rewardClaimFailed": "Ödül alınamadı",
|
||||
"enterRoomConfirmTips": "Odaya girmek istediğinizden emin misiniz?",
|
||||
|
||||
@ -906,6 +906,10 @@ class SCAppLocalizations {
|
||||
|
||||
String get useMicrophoneForOneMin => translate('useMicrophoneForOneMin');
|
||||
|
||||
String get taskRequirement => translate('taskRequirement');
|
||||
|
||||
String get taskProgress => translate('taskProgress');
|
||||
|
||||
String get rewardClaimedSuccessfully =>
|
||||
translate('rewardClaimedSuccessfully');
|
||||
|
||||
|
||||
@ -54,22 +54,34 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_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/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
|
||||
|
||||
const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
);
|
||||
const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2);
|
||||
|
||||
String _resolveVipChatBubbleImageUrl(SCVipResourceRes? resource) {
|
||||
return _firstNonBlankChatBubbleUrl([
|
||||
final candidates = <String?>[
|
||||
resource?.sourceResourceUrl,
|
||||
resource?.previewUrl,
|
||||
resource?.coverUrl,
|
||||
resource?.cover,
|
||||
_staticChatBubbleUrl(resource?.sourceResourceUrl),
|
||||
];
|
||||
return _firstNonBlankChatBubbleUrl([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -83,14 +95,6 @@ String _firstNonBlankChatBubbleUrl(Iterable<String?> values) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String? _staticChatBubbleUrl(String? value) {
|
||||
final text = value?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return SCNetworkSvgaWidget.isSvga(text) ? null : text;
|
||||
}
|
||||
|
||||
class SCMessageChatPage extends StatefulWidget {
|
||||
final V2TimConversation? conversation;
|
||||
final bool shrinkWrap;
|
||||
@ -1698,6 +1702,8 @@ class _MessageItem extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
)
|
||||
: _buildStaticVipBubbleImage(
|
||||
@ -1721,6 +1727,8 @@ class _MessageItem extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
@ -1743,22 +1751,13 @@ class _MessageItem extends StatelessWidget {
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
}
|
||||
|
||||
return Image(
|
||||
image: buildCachedImageProvider(
|
||||
imageUrl,
|
||||
logicalWidth: width,
|
||||
logicalHeight: height,
|
||||
),
|
||||
return SCNinePatchImage(
|
||||
url: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint(
|
||||
'[VIP][ChatBubble] image load failed url=$imageUrl error=$error',
|
||||
);
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
},
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback ?? SizedBox(width: width, height: height),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1775,7 +1774,7 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
final candidates = <String?>[
|
||||
resource?.imSendCoverUrl,
|
||||
resource?.imOpenedUrl,
|
||||
resource?.imOpenedUrlTwo,
|
||||
@ -1786,6 +1785,10 @@ class _MessageItem extends StatelessWidget {
|
||||
resource?.cover,
|
||||
resource?.expand,
|
||||
resource?.sourceUrl,
|
||||
];
|
||||
return _firstNonBlank([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
static const Color _profileBg = Color(0xff072121);
|
||||
static const Color _cardBg = Color(0xff08251E);
|
||||
static const Color _profileBorder = Color(0xffB2FBCC);
|
||||
static const int _wallPreviewItemCount = 9;
|
||||
|
||||
SCUserIdentityRes? userIdentity;
|
||||
|
||||
@ -149,7 +150,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
.giftWall(widget.tageId)
|
||||
.then((result) {
|
||||
if (!mounted) return;
|
||||
giftWallList = result;
|
||||
giftWallList = result.take(_wallPreviewItemCount).toList();
|
||||
setState(() {});
|
||||
})
|
||||
.catchError((e) {});
|
||||
@ -644,7 +645,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
_buildWallGrid(
|
||||
itemCount: 4,
|
||||
itemCount: _wallPreviewItemCount,
|
||||
itemBuilder: (_) => _buildHonorPlaceholderItem(),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
@ -659,7 +660,10 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
_buildWallGrid(
|
||||
itemCount: giftWallList.isEmpty ? 4 : giftWallList.length,
|
||||
itemCount:
|
||||
giftWallList.isEmpty
|
||||
? _wallPreviewItemCount
|
||||
: giftWallList.length,
|
||||
itemBuilder: (index) {
|
||||
final gift =
|
||||
index < giftWallList.length ? giftWallList[index] : null;
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/main.dart' show routeObserver;
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
@ -17,7 +18,8 @@ class TaskPage extends StatefulWidget {
|
||||
State<TaskPage> createState() => _TaskPageState();
|
||||
}
|
||||
|
||||
class _TaskPageState extends State<TaskPage> {
|
||||
class _TaskPageState extends State<TaskPage>
|
||||
with WidgetsBindingObserver, RouteAware {
|
||||
static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png';
|
||||
static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png';
|
||||
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
||||
@ -28,22 +30,59 @@ class _TaskPageState extends State<TaskPage> {
|
||||
final Set<String> _claimingTaskIds = {};
|
||||
List<SCTaskListRes> _tasks = [];
|
||||
bool _loading = true;
|
||||
bool _loadingTasks = false;
|
||||
String? _toastMessage;
|
||||
Timer? _toastTimer;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_loadTasks();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
routeObserver.unsubscribe(this);
|
||||
_toastTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
|
||||
if (_routeObserverRoute != null) {
|
||||
routeObserver.unsubscribe(this);
|
||||
}
|
||||
_routeObserverRoute = route;
|
||||
routeObserver.subscribe(this, route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
debugPrint('[TaskCenter][Page] refresh on app resumed');
|
||||
unawaited(_loadTasks());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
debugPrint('[TaskCenter][Page] refresh on route return');
|
||||
unawaited(_loadTasks());
|
||||
}
|
||||
|
||||
Future<void> _loadTasks() async {
|
||||
if (_loadingTasks) {
|
||||
debugPrint('[TaskCenter][Page] load skipped: request in flight');
|
||||
return;
|
||||
}
|
||||
_loadingTasks = true;
|
||||
debugPrint('[TaskCenter][Page] load start');
|
||||
try {
|
||||
final tasks = await _repository.tasks();
|
||||
@ -69,6 +108,8 @@ class _TaskPageState extends State<TaskPage> {
|
||||
_tasks = [];
|
||||
_loading = false;
|
||||
});
|
||||
} finally {
|
||||
_loadingTasks = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,13 +296,13 @@ class _TaskPageState extends State<TaskPage> {
|
||||
_TaskUiItem _mapTask(SCTaskListRes task) {
|
||||
final id = task.taskId ?? '';
|
||||
final currentValue = _parseNumber(task.completedValue) ?? 0;
|
||||
final targetValue =
|
||||
_parseNumber(task.targetValue) ??
|
||||
_parseNumber(task.conditionValue) ??
|
||||
1000000;
|
||||
final parsedTargetValue =
|
||||
_parseNumber(task.targetValue) ?? _parseNumber(task.conditionValue);
|
||||
final targetValue = parsedTargetValue ?? 0;
|
||||
final hasProgress = parsedTargetValue != null && parsedTargetValue > 0;
|
||||
final claimed =
|
||||
_claimedTaskIds.contains(id) || (task.isRewardCollected ?? 0) == 1;
|
||||
final completedByValue = targetValue > 0 && currentValue >= targetValue;
|
||||
final completedByValue = hasProgress && currentValue >= targetValue;
|
||||
final completedByStatus = (task.taskStatus ?? 0) == 1;
|
||||
final status =
|
||||
claimed
|
||||
@ -275,16 +316,27 @@ class _TaskPageState extends State<TaskPage> {
|
||||
title:
|
||||
_nonEmpty(task.taskName) ??
|
||||
SCAppLocalizations.of(context)!.useMicrophoneForOneMin,
|
||||
progressText:
|
||||
_nonEmpty(task.taskDesc) ??
|
||||
'${_formatPlainNumber(currentValue)}/${_formatPlainNumber(targetValue)}',
|
||||
rewardText: _formatCompactNumber(task.quantity ?? 1100),
|
||||
progressText: _taskProgressText(
|
||||
task,
|
||||
currentValue: currentValue,
|
||||
targetValue: targetValue,
|
||||
hasProgress: hasProgress,
|
||||
),
|
||||
requirementText: _taskRequirementText(
|
||||
task,
|
||||
targetValue: targetValue,
|
||||
hasProgress: hasProgress,
|
||||
),
|
||||
rewardText: _formatPlainNumber(task.quantity ?? 1100),
|
||||
status: status,
|
||||
iconAsset: _iconForTask(task),
|
||||
iconUrl: _nonEmpty(task.taskIcon) ?? _nonEmpty(task.cover),
|
||||
jumpPage: _nonEmpty(task.jumpPage),
|
||||
jumpType: _nonEmpty(task.jumpType),
|
||||
conditionType: _nonEmpty(task.conditionType),
|
||||
roomId: _nonEmpty(task.roomId),
|
||||
roomTask: _isRoomCompletionTask(task),
|
||||
gameTask: _isGameCompletionTask(task),
|
||||
fromApi: id.isNotEmpty,
|
||||
);
|
||||
}
|
||||
@ -292,12 +344,18 @@ class _TaskPageState extends State<TaskPage> {
|
||||
Future<void> _handleTaskAction(_TaskUiItem task) async {
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] action id=${task.id} status=${task.status.name} '
|
||||
'fromApi=${task.fromApi} jumpType=${task.jumpType} '
|
||||
'jumpPage=${task.jumpPage} roomId=${task.roomId}',
|
||||
'fromApi=${task.fromApi} conditionType=${task.conditionType} '
|
||||
'jumpType=${task.jumpType} jumpPage=${task.jumpPage} '
|
||||
'roomId=${task.roomId} roomTask=${task.roomTask} '
|
||||
'gameTask=${task.gameTask}',
|
||||
);
|
||||
if (task.status == _TaskActionStatus.claimed) return;
|
||||
if (task.status == _TaskActionStatus.go) {
|
||||
await _openJumpPage(task);
|
||||
final opened = await _openJumpPage(task);
|
||||
if (opened && mounted) {
|
||||
debugPrint('[TaskCenter][Page] refresh after jump return');
|
||||
unawaited(_loadTasks());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -324,9 +382,9 @@ class _TaskPageState extends State<TaskPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openJumpPage(_TaskUiItem task) async {
|
||||
Future<bool> _openJumpPage(_TaskUiItem task) async {
|
||||
var target = _taskJumpUrl(task);
|
||||
if (target.isEmpty) return;
|
||||
if (target.isEmpty) return false;
|
||||
var uri = Uri.tryParse(target);
|
||||
var path = uri?.path ?? '';
|
||||
var roomId = uri?.queryParameters['roomId']?.trim() ?? '';
|
||||
@ -336,10 +394,10 @@ class _TaskPageState extends State<TaskPage> {
|
||||
'target=$target taskId=${task.id}',
|
||||
);
|
||||
final fallbackTarget = await _firstRoomJumpUrl(target);
|
||||
if (!mounted) return;
|
||||
if (!mounted) return false;
|
||||
if (fallbackTarget.isEmpty) {
|
||||
_showToast(SCAppLocalizations.of(context)!.enterRoomFailedRetry);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
target = fallbackTarget;
|
||||
uri = Uri.tryParse(target);
|
||||
@ -357,6 +415,7 @@ class _TaskPageState extends State<TaskPage> {
|
||||
jumpType: jumpType,
|
||||
jumpUrl: target,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> _firstRoomJumpUrl(String target) async {
|
||||
@ -393,19 +452,7 @@ class _TaskPageState extends State<TaskPage> {
|
||||
final target = task.jumpPage?.trim() ?? '';
|
||||
final roomId = task.roomId?.trim() ?? '';
|
||||
if (target.isEmpty) {
|
||||
if (_isGameJumpType(task.jumpType)) {
|
||||
return roomId.isEmpty
|
||||
? '/game-center'
|
||||
: '/game-center?roomId=${Uri.encodeComponent(roomId)}';
|
||||
}
|
||||
if (_isRoomJumpType(task.jumpType)) {
|
||||
return roomId.isEmpty
|
||||
? '/room'
|
||||
: '/room?roomId=${Uri.encodeComponent(roomId)}';
|
||||
}
|
||||
return roomId.isEmpty
|
||||
? ''
|
||||
: '/room?roomId=${Uri.encodeComponent(roomId)}';
|
||||
return _defaultTaskJumpUrl(task, roomId);
|
||||
}
|
||||
if (roomId.isEmpty) return target;
|
||||
|
||||
@ -415,20 +462,42 @@ class _TaskPageState extends State<TaskPage> {
|
||||
return _withRoomId(target, roomId);
|
||||
}
|
||||
|
||||
String _defaultTaskJumpUrl(_TaskUiItem task, String roomId) {
|
||||
if (_isGameJumpType(task.jumpType) || task.gameTask) {
|
||||
return _routeWithOptionalRoomId('/game-center', roomId);
|
||||
}
|
||||
if (_isRoomJumpType(task.jumpType) || task.roomTask) {
|
||||
return _routeWithOptionalRoomId('/room', roomId);
|
||||
}
|
||||
if (_isRechargeTask(task)) {
|
||||
return '/recharge';
|
||||
}
|
||||
return roomId.isEmpty ? '' : _routeWithOptionalRoomId('/room', roomId);
|
||||
}
|
||||
|
||||
String _routeWithOptionalRoomId(String route, String roomId) {
|
||||
if (roomId.isEmpty) return route;
|
||||
return '$route?roomId=${Uri.encodeComponent(roomId)}';
|
||||
}
|
||||
|
||||
String _taskJumpType(_TaskUiItem task, String target) {
|
||||
final uri = Uri.tryParse(target);
|
||||
final path = uri?.path ?? '';
|
||||
final jumpType = task.jumpType?.trim();
|
||||
if (jumpType != null && jumpType.isNotEmpty) {
|
||||
if (_isGameJumpType(jumpType)) return 'GAME';
|
||||
if (_isRoomJumpType(jumpType)) return 'APP_ROUTE';
|
||||
return jumpType;
|
||||
}
|
||||
final uri = Uri.tryParse(target);
|
||||
if (uri?.hasScheme == true) {
|
||||
return 'H5';
|
||||
}
|
||||
if (uri?.path == '/game-center') {
|
||||
if (path == '/game-center') {
|
||||
return 'GAME';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
return 'APP_ROUTE';
|
||||
}
|
||||
if (jumpType != null && jumpType.isNotEmpty) return jumpType;
|
||||
return 'APP_ROUTE';
|
||||
}
|
||||
|
||||
@ -436,11 +505,16 @@ class _TaskPageState extends State<TaskPage> {
|
||||
final normalized = jumpType?.trim().toUpperCase() ?? '';
|
||||
return normalized == 'ROOM' ||
|
||||
normalized == 'VOICE_ROOM' ||
|
||||
normalized == 'VOICECHAT_ROOM';
|
||||
normalized == 'VOICECHAT_ROOM' ||
|
||||
normalized == 'ENTER_ROOM' ||
|
||||
normalized == 'JOIN_ROOM';
|
||||
}
|
||||
|
||||
bool _isGameJumpType(String? jumpType) {
|
||||
return jumpType?.trim().toUpperCase() == 'GAME';
|
||||
final normalized = jumpType?.trim().toUpperCase() ?? '';
|
||||
return normalized == 'GAME' ||
|
||||
normalized == 'GAME_CENTER' ||
|
||||
normalized == 'GAME_CONSUME_GOLD';
|
||||
}
|
||||
|
||||
void _showToast(String message) {
|
||||
@ -461,9 +535,79 @@ class _TaskPageState extends State<TaskPage> {
|
||||
return _taskDefaultAsset;
|
||||
}
|
||||
|
||||
String _taskProgressText(
|
||||
SCTaskListRes task, {
|
||||
required num currentValue,
|
||||
required num targetValue,
|
||||
required bool hasProgress,
|
||||
}) {
|
||||
if (!hasProgress) return '';
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
return '${l10n.taskProgress}: '
|
||||
'${_formatTaskValue(task, currentValue)}/${_formatTaskValue(task, targetValue)}';
|
||||
}
|
||||
|
||||
String _taskRequirementText(
|
||||
SCTaskListRes task, {
|
||||
required num targetValue,
|
||||
required bool hasProgress,
|
||||
}) {
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
if (hasProgress) {
|
||||
return '${l10n.taskRequirement}: ${_formatTaskValue(task, targetValue)}';
|
||||
}
|
||||
return _nonEmpty(task.taskDesc) ?? '';
|
||||
}
|
||||
|
||||
bool _isGameCompletionTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('GAME')) return true;
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
return text.contains('game') || text.contains('游戏');
|
||||
}
|
||||
|
||||
bool _isRoomCompletionTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('RECHARGE')) return false;
|
||||
if (_isGameCompletionTask(task)) return false;
|
||||
if (conditionType.contains('MIC') ||
|
||||
conditionType.contains('VOICE') ||
|
||||
conditionType.contains('ROOM') ||
|
||||
conditionType.contains('GIFT') ||
|
||||
conditionType.contains('CONSUME')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
if (text.contains('recharge') || text.contains('充值')) return false;
|
||||
if (text.contains('game') || text.contains('游戏')) return false;
|
||||
return text.contains('mic') ||
|
||||
text.contains('voice') ||
|
||||
text.contains('room') ||
|
||||
text.contains('gift') ||
|
||||
text.contains('consume') ||
|
||||
text.contains('spend') ||
|
||||
text.contains('上麦') ||
|
||||
text.contains('房间') ||
|
||||
text.contains('语音') ||
|
||||
text.contains('礼物') ||
|
||||
text.contains('消费');
|
||||
}
|
||||
|
||||
bool _isRechargeTask(_TaskUiItem task) {
|
||||
final text =
|
||||
'${task.conditionType ?? ''} ${task.title} ${task.jumpType ?? ''}'
|
||||
.toLowerCase();
|
||||
return text.contains('recharge') || text.contains('充值');
|
||||
}
|
||||
|
||||
static num? _parseNumber(Object? value) {
|
||||
if (value is num) return value;
|
||||
if (value is String) return num.tryParse(value);
|
||||
if (value is String) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) return null;
|
||||
return num.tryParse(text) ?? num.tryParse(text.replaceAll(',', ''));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -477,20 +621,49 @@ class _TaskPageState extends State<TaskPage> {
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
static String _formatCompactNumber(num value) {
|
||||
final absValue = value.abs();
|
||||
if (absValue >= 1000000) {
|
||||
return '${_trimTrailingZero(value / 1000000)}m';
|
||||
}
|
||||
if (absValue >= 1000) {
|
||||
return '${_trimTrailingZero(value / 1000)}k';
|
||||
}
|
||||
String _formatTaskValue(SCTaskListRes task, num value) {
|
||||
if (_isTimeValueTask(task)) return _formatDurationSeconds(value);
|
||||
return _formatPlainNumber(value);
|
||||
}
|
||||
|
||||
static String _trimTrailingZero(num value) {
|
||||
final text = value.toStringAsFixed(1);
|
||||
return text.endsWith('.0') ? text.substring(0, text.length - 2) : text;
|
||||
bool _isTimeValueTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('GOLD') || conditionType.contains('CONSUME')) {
|
||||
return false;
|
||||
}
|
||||
if (conditionType.contains('SECOND') ||
|
||||
conditionType.contains('DURATION') ||
|
||||
RegExp(r'(^|_)TIME($|_)').hasMatch(conditionType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
return text.contains('second') ||
|
||||
text.contains('seconds') ||
|
||||
text.contains('minute') ||
|
||||
text.contains('minutes') ||
|
||||
text.contains('hour') ||
|
||||
text.contains('hours') ||
|
||||
text.contains(' min') ||
|
||||
text.contains('秒') ||
|
||||
text.contains('分钟') ||
|
||||
text.contains('小时');
|
||||
}
|
||||
|
||||
static String _formatDurationSeconds(num seconds) {
|
||||
final absSeconds = seconds.abs();
|
||||
if (absSeconds >= 3600) {
|
||||
return '${_formatShortDecimal(seconds / 3600)}h';
|
||||
}
|
||||
if (absSeconds >= 60) {
|
||||
return '${_formatShortDecimal(seconds / 60)}min';
|
||||
}
|
||||
return '${_formatShortDecimal(seconds)}s';
|
||||
}
|
||||
|
||||
static String _formatShortDecimal(num value) {
|
||||
final fixed = value.toStringAsFixed(1);
|
||||
return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed;
|
||||
}
|
||||
|
||||
String _taskDebugSample(List<SCTaskListRes> tasks) {
|
||||
@ -502,6 +675,7 @@ class _TaskPageState extends State<TaskPage> {
|
||||
'status=${task.taskStatus},collected=${task.isRewardCollected},'
|
||||
'current=${task.completedValue},target=${task.targetValue},'
|
||||
'quantity=${task.quantity},name=${task.taskName},'
|
||||
'condition=${task.conditionType},'
|
||||
'jumpType=${task.jumpType},jumpPage=${task.jumpPage},'
|
||||
'roomId=${task.roomId}}';
|
||||
})
|
||||
@ -671,7 +845,7 @@ class _TaskRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 65.w,
|
||||
height: 76.w,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 12.w),
|
||||
@ -703,18 +877,35 @@ class _TaskRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
if (task.requirementText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.requirementText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
SizedBox(height: 3.w),
|
||||
],
|
||||
if (task.progressText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
] else
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
@ -903,26 +1094,34 @@ class _TaskUiItem {
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.progressText,
|
||||
required this.requirementText,
|
||||
required this.rewardText,
|
||||
required this.status,
|
||||
required this.iconAsset,
|
||||
this.iconUrl,
|
||||
this.jumpPage,
|
||||
this.jumpType,
|
||||
this.conditionType,
|
||||
this.roomId,
|
||||
this.roomTask = false,
|
||||
this.gameTask = false,
|
||||
this.fromApi = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String progressText;
|
||||
final String requirementText;
|
||||
final String rewardText;
|
||||
final _TaskActionStatus status;
|
||||
final String iconAsset;
|
||||
final String? iconUrl;
|
||||
final String? jumpPage;
|
||||
final String? jumpType;
|
||||
final String? conditionType;
|
||||
final String? roomId;
|
||||
final bool roomTask;
|
||||
final bool gameTask;
|
||||
final bool fromApi;
|
||||
|
||||
_TaskUiItem copyWith({_TaskActionStatus? status}) {
|
||||
@ -930,13 +1129,17 @@ class _TaskUiItem {
|
||||
id: id,
|
||||
title: title,
|
||||
progressText: progressText,
|
||||
requirementText: requirementText,
|
||||
rewardText: rewardText,
|
||||
status: status ?? this.status,
|
||||
iconAsset: iconAsset,
|
||||
iconUrl: iconUrl,
|
||||
jumpPage: jumpPage,
|
||||
jumpType: jumpType,
|
||||
conditionType: conditionType,
|
||||
roomId: roomId,
|
||||
roomTask: roomTask,
|
||||
gameTask: gameTask,
|
||||
fromApi: fromApi,
|
||||
);
|
||||
}
|
||||
|
||||
@ -350,6 +350,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
int? _pendingSelfMicReleaseGuardUntilMs;
|
||||
ClientRoleType? _lastAppliedClientRole;
|
||||
bool? _lastAppliedLocalAudioMuted;
|
||||
bool? _lastAppliedRecordingSignalMuted;
|
||||
String? _lastAppliedRtcToken;
|
||||
bool? _lastScheduledVoiceLiveOnMic;
|
||||
String? _lastScheduledVoiceLiveRoomId;
|
||||
String? _lastScheduledAnchorRoomId;
|
||||
@ -1091,6 +1093,20 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
.connectionChangedLicenseValidationFailure;
|
||||
}
|
||||
|
||||
MicRes? _currentUserMicSeat() {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final mic in roomWheatMap.values) {
|
||||
if ((mic.user?.id ?? "").trim() == currentUserId) {
|
||||
return mic;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _syncSelfMicRuntimeState() {
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if ((currentUserId ?? "").isEmpty) {
|
||||
@ -1098,13 +1114,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
|
||||
MicRes? currentUserMic;
|
||||
for (final mic in roomWheatMap.values) {
|
||||
if (mic.user?.id == currentUserId) {
|
||||
currentUserMic = mic;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final currentUserMic = _currentUserMicSeat();
|
||||
|
||||
if (currentUserMic == null) {
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
@ -1133,6 +1143,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_applyLocalAudioRuntimeState(
|
||||
clientRole: ClientRoleType.clientRoleBroadcaster,
|
||||
muted: (currentUserMic.micMute ?? false) || isMic,
|
||||
publisherToken: currentUserMic.roomToken,
|
||||
);
|
||||
_roomMusicMicRouteChangedListener?.call(true);
|
||||
}
|
||||
@ -1211,6 +1222,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
void _applyLocalAudioRuntimeState({
|
||||
required ClientRoleType clientRole,
|
||||
required bool muted,
|
||||
String? publisherToken,
|
||||
}) {
|
||||
if (clientRole != ClientRoleType.clientRoleBroadcaster) {
|
||||
_agoraRoleIsBroadcaster = false;
|
||||
@ -1218,19 +1230,39 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (engine == null) {
|
||||
_lastAppliedClientRole = null;
|
||||
_lastAppliedLocalAudioMuted = null;
|
||||
_lastAppliedRecordingSignalMuted = null;
|
||||
_lastAppliedRtcToken = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedPublisherToken = (publisherToken ?? "").trim();
|
||||
if (clientRole == ClientRoleType.clientRoleBroadcaster &&
|
||||
normalizedPublisherToken.isNotEmpty &&
|
||||
_lastAppliedRtcToken != normalizedPublisherToken) {
|
||||
engine?.renewToken(normalizedPublisherToken);
|
||||
_lastAppliedRtcToken = normalizedPublisherToken;
|
||||
}
|
||||
|
||||
if (_lastAppliedClientRole != clientRole) {
|
||||
engine?.setClientRole(role: clientRole);
|
||||
_lastAppliedClientRole = clientRole;
|
||||
}
|
||||
if (!muted && _lastAppliedLocalAudioMuted != false) {
|
||||
adjustRecordingSignalVolume(100);
|
||||
|
||||
final shouldPublishLocalAudio =
|
||||
clientRole == ClientRoleType.clientRoleBroadcaster;
|
||||
final localAudioMuted = !shouldPublishLocalAudio;
|
||||
if (_lastAppliedLocalAudioMuted != localAudioMuted) {
|
||||
engine?.muteLocalAudioStream(localAudioMuted);
|
||||
_lastAppliedLocalAudioMuted = localAudioMuted;
|
||||
}
|
||||
if (_lastAppliedLocalAudioMuted != muted) {
|
||||
engine?.muteLocalAudioStream(muted);
|
||||
_lastAppliedLocalAudioMuted = muted;
|
||||
|
||||
final recordingSignalMuted = shouldPublishLocalAudio ? muted : true;
|
||||
if (_lastAppliedRecordingSignalMuted != recordingSignalMuted) {
|
||||
engine?.muteRecordingSignal(recordingSignalMuted);
|
||||
_lastAppliedRecordingSignalMuted = recordingSignalMuted;
|
||||
}
|
||||
if (shouldPublishLocalAudio && !recordingSignalMuted) {
|
||||
adjustRecordingSignalVolume(100);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1243,6 +1275,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
void _resetLocalAudioRuntimeTracking() {
|
||||
_lastAppliedClientRole = null;
|
||||
_lastAppliedLocalAudioMuted = null;
|
||||
_lastAppliedRecordingSignalMuted = null;
|
||||
_lastAppliedRtcToken = null;
|
||||
}
|
||||
|
||||
void _applyMicSnapshot(
|
||||
@ -1561,14 +1595,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
try {
|
||||
if (publisherToken.trim().isNotEmpty) {
|
||||
await engine?.renewToken(publisherToken);
|
||||
_lastAppliedRtcToken = publisherToken.trim();
|
||||
}
|
||||
await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||||
await engine?.muteLocalAudioStream(false);
|
||||
await engine?.muteRecordingSignal(muted);
|
||||
if (!muted) {
|
||||
adjustRecordingSignalVolume(100);
|
||||
await engine?.adjustRecordingSignalVolume(100);
|
||||
}
|
||||
await engine?.muteLocalAudioStream(muted);
|
||||
_lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster;
|
||||
_lastAppliedLocalAudioMuted = muted;
|
||||
_lastAppliedLocalAudioMuted = false;
|
||||
_lastAppliedRecordingSignalMuted = muted;
|
||||
_agoraRoleIsBroadcaster = true;
|
||||
_resyncHeartbeatFromAgoraState();
|
||||
return true;
|
||||
@ -1584,8 +1621,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
try {
|
||||
await engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
await engine?.muteLocalAudioStream(true);
|
||||
await engine?.muteRecordingSignal(true);
|
||||
_lastAppliedClientRole = ClientRoleType.clientRoleAudience;
|
||||
_lastAppliedLocalAudioMuted = true;
|
||||
_lastAppliedRecordingSignalMuted = true;
|
||||
_lastAppliedRtcToken = null;
|
||||
_agoraRoleIsBroadcaster = false;
|
||||
_resyncHeartbeatFromAgoraState();
|
||||
return true;
|
||||
@ -4028,6 +4068,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (filePath.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!loopback) {
|
||||
final canPublish = await _ensureRoomMusicPublishReady();
|
||||
if (!canPublish) {
|
||||
throw StateError('Current user is not ready to publish room music');
|
||||
}
|
||||
}
|
||||
await engine?.startAudioMixing(
|
||||
filePath: filePath,
|
||||
loopback: loopback,
|
||||
@ -4065,4 +4111,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
await engine?.adjustAudioMixingPlayoutVolume(safeVolume);
|
||||
await engine?.adjustAudioMixingPublishVolume(safeVolume);
|
||||
}
|
||||
|
||||
Future<bool> _ensureRoomMusicPublishReady() async {
|
||||
if (!_agoraJoined || engine == null) {
|
||||
return false;
|
||||
}
|
||||
final currentUserMic = _currentUserMicSeat();
|
||||
if (currentUserMic == null) {
|
||||
return false;
|
||||
}
|
||||
return _switchAgoraToBroadcaster(
|
||||
publisherToken: currentUserMic.roomToken ?? "",
|
||||
muted: (currentUserMic.micMute ?? false) || isMic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,30 +68,93 @@ class SCTaskListRes {
|
||||
map['route'],
|
||||
map['actionUrl'],
|
||||
map['targetUrl'],
|
||||
_nestedValue(map, 'jumpPage'),
|
||||
_nestedValue(map, 'jumpUrl'),
|
||||
_nestedValue(map, 'route'),
|
||||
_nestedValue(map, 'targetUrl'),
|
||||
]);
|
||||
_jumpType = _firstString([
|
||||
map['jumpType'],
|
||||
map['actionType'],
|
||||
map['clickType'],
|
||||
map['targetType'],
|
||||
_nestedValue(map, 'jumpType'),
|
||||
_nestedValue(map, 'actionType'),
|
||||
_nestedValue(map, 'targetType'),
|
||||
]);
|
||||
_roomId = _firstString([
|
||||
map['roomId'],
|
||||
map['targetRoomId'],
|
||||
map['jumpRoomId'],
|
||||
_nestedValue(map, 'roomId'),
|
||||
_nestedValue(map, 'targetRoomId'),
|
||||
_nestedValue(map, 'jumpRoomId'),
|
||||
]);
|
||||
_conditionType = _firstString([
|
||||
map['conditionType'],
|
||||
map['conditionCode'],
|
||||
_nestedValue(map, 'conditionType'),
|
||||
_nestedValue(map, 'conditionCode'),
|
||||
]);
|
||||
_conditionType = _asString(map['conditionType']);
|
||||
_sortOrder = _asNum(map['sortOrder']);
|
||||
_taskId = _asString(map['taskId']);
|
||||
_taskStatus = _asNum(map['taskStatus']);
|
||||
_cover = _asString(map['cover']);
|
||||
_taskIcon = _asString(map['taskIcon']);
|
||||
_conditionValue = _asString(map['conditionValue']);
|
||||
_completedValue = _asNum(map['completedValue']);
|
||||
_conditionValue =
|
||||
_asString(map['conditionValue']) ??
|
||||
_asString(_nestedValue(map, 'conditionValue'));
|
||||
_completedValue = _firstNum([
|
||||
map['completedValue'],
|
||||
map['currentValue'],
|
||||
map['currentProgress'],
|
||||
map['progressValue'],
|
||||
map['finishValue'],
|
||||
map['finishedValue'],
|
||||
map['completedCount'],
|
||||
map['completeCount'],
|
||||
map['finishCount'],
|
||||
map['finishedCount'],
|
||||
map['doneCount'],
|
||||
map['current'],
|
||||
map['currentNum'],
|
||||
map['currentAmount'],
|
||||
_nestedValue(map, 'completedValue'),
|
||||
_nestedValue(map, 'currentValue'),
|
||||
_nestedValue(map, 'current'),
|
||||
_nestedValue(map, 'progressValue'),
|
||||
_nestedValue(map, 'completedCount'),
|
||||
_nestedValue(map, 'currentNum'),
|
||||
_nestedValue(map, 'currentAmount'),
|
||||
]);
|
||||
_isRewardCollected = _asNum(map['isRewardCollected']);
|
||||
_quantity = _asNum(map['quantity']);
|
||||
_targetValue = _asNum(map['targetValue']);
|
||||
_targetValue = _firstNum([
|
||||
map['targetValue'],
|
||||
map['target'],
|
||||
map['goalValue'],
|
||||
map['requiredValue'],
|
||||
map['conditionValue'],
|
||||
map['conditionNum'],
|
||||
map['targetCount'],
|
||||
map['requiredCount'],
|
||||
map['targetNum'],
|
||||
map['requiredNum'],
|
||||
map['total'],
|
||||
_nestedValue(map, 'targetValue'),
|
||||
_nestedValue(map, 'target'),
|
||||
_nestedValue(map, 'goalValue'),
|
||||
_nestedValue(map, 'requiredValue'),
|
||||
_nestedValue(map, 'conditionValue'),
|
||||
_nestedValue(map, 'targetNum'),
|
||||
_nestedValue(map, 'requiredNum'),
|
||||
_nestedValue(map, 'total'),
|
||||
]);
|
||||
if (_completedValue == null &&
|
||||
_targetValue != null &&
|
||||
(_taskStatus ?? 0) == 1) {
|
||||
_completedValue = _targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
num? _taskType;
|
||||
@ -208,6 +271,15 @@ class SCTaskListRes {
|
||||
'extra',
|
||||
'ext',
|
||||
'extend',
|
||||
'progress',
|
||||
'taskProgress',
|
||||
'condition',
|
||||
'taskCondition',
|
||||
'completion',
|
||||
'completionInfo',
|
||||
'progressInfo',
|
||||
'stat',
|
||||
'stats',
|
||||
];
|
||||
for (final nestedKey in nestedKeys) {
|
||||
final nested = _asMap(map[nestedKey]);
|
||||
@ -220,7 +292,21 @@ class SCTaskListRes {
|
||||
|
||||
static num? _asNum(dynamic value) {
|
||||
if (value is num) return value;
|
||||
if (value is String) return num.tryParse(value);
|
||||
if (value is String) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) return null;
|
||||
return num.tryParse(text) ?? num.tryParse(text.replaceAll(',', ''));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static num? _firstNum(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final number = _asNum(value);
|
||||
if (number != null) {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1020,7 +1020,9 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
return "{id=${task.taskId},type=${task.taskType},"
|
||||
"status=${task.taskStatus},collected=${task.isRewardCollected},"
|
||||
"current=${task.completedValue},target=${task.targetValue},"
|
||||
"quantity=${task.quantity},name=${task.taskName}}";
|
||||
"quantity=${task.quantity},name=${task.taskName},"
|
||||
"condition=${task.conditionType},jumpType=${task.jumpType},"
|
||||
"jumpPage=${task.jumpPage},roomId=${task.roomId}}";
|
||||
})
|
||||
.join(" ");
|
||||
return "sample=$sample";
|
||||
|
||||
@ -605,8 +605,22 @@ class _RewardCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final horizontalPadding = item.isFinalDay ? 12 * scale : 7 * scale;
|
||||
final topPadding = item.isFinalDay ? 12 * scale : 18 * scale;
|
||||
final bottomPadding = item.isFinalDay ? 5 * scale : 7 * scale;
|
||||
final topPadding =
|
||||
item.isCoinReward
|
||||
? (item.isFinalDay ? 10 * scale : 14 * scale)
|
||||
: (item.isFinalDay ? 12 * scale : 18 * scale);
|
||||
final bottomPadding =
|
||||
item.isCoinReward
|
||||
? 4 * scale
|
||||
: (item.isFinalDay ? 5 * scale : 7 * scale);
|
||||
final coverSpacing = item.isCoinReward ? 3 * scale : 5 * scale;
|
||||
final subtitleSpacing = item.isCoinReward ? 1 * scale : 2 * scale;
|
||||
final double titleFontSize =
|
||||
item.isCoinReward
|
||||
? (item.isFinalDay ? 8.5 : 8.0)
|
||||
: (item.isFinalDay ? 9.0 : 8.2);
|
||||
final double subtitleFontSize = item.isCoinReward ? 6.5 : 7.0;
|
||||
final double titleLineHeight = item.isCoinReward ? 1.0 : 1.2;
|
||||
|
||||
final titleColor =
|
||||
item.status == DailySignInRewardStatus.current
|
||||
@ -671,23 +685,24 @@ class _RewardCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_RewardCover(item: item, scale: scale),
|
||||
SizedBox(height: 5 * scale),
|
||||
SizedBox(height: coverSpacing),
|
||||
text(
|
||||
item.title,
|
||||
fontSize: item.isFinalDay ? 9 : 8.2,
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.w700,
|
||||
maxLines: 2,
|
||||
lineHeight: 1.2,
|
||||
lineHeight: titleLineHeight,
|
||||
textAlign: TextAlign.center,
|
||||
textColor: titleColor,
|
||||
),
|
||||
if (item.subtitle.isNotEmpty) ...[
|
||||
SizedBox(height: 2 * scale),
|
||||
SizedBox(height: subtitleSpacing),
|
||||
text(
|
||||
item.subtitle,
|
||||
fontSize: 7,
|
||||
fontSize: subtitleFontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
maxLines: 1,
|
||||
lineHeight: 1.0,
|
||||
textAlign: TextAlign.center,
|
||||
textColor: subtitleColor,
|
||||
),
|
||||
@ -727,7 +742,7 @@ class _RewardCover extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final size =
|
||||
item.isCoinReward
|
||||
? (item.isFinalDay ? 36 * scale : 34 * scale)
|
||||
? (item.isFinalDay ? 31 * scale : 29 * scale)
|
||||
: (item.isFinalDay ? 30 * scale : 28 * scale);
|
||||
final cover = item.cover;
|
||||
final borderColor =
|
||||
|
||||
@ -64,6 +64,14 @@ class _RoomRedPacketClaimRecordPageState
|
||||
_RoomRedPacketClaimRecordTopBar(
|
||||
title: localizations.roomRedPacketClaimRecordsTitle,
|
||||
),
|
||||
_RoomRedPacketClaimSummary(record: widget.record),
|
||||
Divider(
|
||||
height: 1.w,
|
||||
thickness: 0.5.w,
|
||||
indent: 16.w,
|
||||
endIndent: 16.w,
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
_loading
|
||||
@ -156,6 +164,98 @@ class _RoomRedPacketClaimRecordPageState
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketClaimSummary extends StatelessWidget {
|
||||
const _RoomRedPacketClaimSummary({required this.record});
|
||||
|
||||
final SCRoomRedPacketSentRecordRes record;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(16.w, 8.w, 16.w, 8.w),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: 58.w),
|
||||
child: Row(
|
||||
textDirection: Directionality.of(context),
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
localizations.roomRedPacketHistoryItemTitle(
|
||||
record.totalCount.round().toString(),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
_formatHistoryTime(record.createTime),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.45),
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatSignedAmount(-record.totalAmount.round()),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (record.refundAmount > 0) ...[
|
||||
SizedBox(height: 3.w),
|
||||
Text(
|
||||
localizations.roomRedPacketHistoryRefund(
|
||||
_bidiSafeLtr(_formatAmount(record.refundAmount.round())),
|
||||
),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF332B),
|
||||
fontSize: 9.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketClaimRow extends StatelessWidget {
|
||||
const _RoomRedPacketClaimRow({required this.item});
|
||||
|
||||
@ -349,6 +449,11 @@ String _formatAmount(int value) {
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String _formatSignedAmount(int value) {
|
||||
final sign = value < 0 ? '-' : '+';
|
||||
return '$sign${_formatAmount(value.abs())}';
|
||||
}
|
||||
|
||||
String _formatHistoryTime(String raw) {
|
||||
final parsed = DateTime.tryParse(raw);
|
||||
if (parsed == null) {
|
||||
@ -358,3 +463,5 @@ String _formatHistoryTime(String raw) {
|
||||
return '${parsed.year}.${two(parsed.month)}.${two(parsed.day)} '
|
||||
'${two(parsed.hour)}:${two(parsed.minute)}:${two(parsed.second)}';
|
||||
}
|
||||
|
||||
String _bidiSafeLtr(String value) => '\u200E$value\u200E';
|
||||
|
||||
@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
@ -536,7 +538,12 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
||||
SCTts.show(localizations.roomRedPacketSendSuccess);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SCTts.show(roomRedPacketErrorMessage(context, error));
|
||||
if (roomRedPacketErrorCode(error) ==
|
||||
'voice_room_red_packet_insufficient_balance') {
|
||||
_showInsufficientBalanceRechargeSheet();
|
||||
} else {
|
||||
SCTts.show(roomRedPacketErrorMessage(context, error));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -622,6 +629,11 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showInsufficientBalanceRechargeSheet() {
|
||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||
_RoomRedPacketRechargeSheet.show(context);
|
||||
}
|
||||
|
||||
void _openRulePage() {
|
||||
final navigator = navigatorKey.currentState;
|
||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||
@ -682,6 +694,225 @@ class _PickupOption {
|
||||
final int minutes;
|
||||
}
|
||||
|
||||
class _RoomRedPacketRechargeSheet extends StatelessWidget {
|
||||
const _RoomRedPacketRechargeSheet();
|
||||
|
||||
static const String dialogTag = 'showRoomRedPacketRechargeSheet';
|
||||
|
||||
static void show(BuildContext context) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black.withValues(alpha: 0.45),
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => const _RoomRedPacketRechargeSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final minSheetHeight = 320.w + bottomPadding;
|
||||
final preferredSheetHeight = MediaQuery.of(context).size.height * 0.46;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height:
|
||||
preferredSheetHeight < minSheetHeight
|
||||
? minSheetHeight
|
||||
: preferredSheetHeight,
|
||||
padding: EdgeInsets.fromLTRB(22.w, 10.w, 22.w, 18.w + bottomPadding),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFFBF5), Color(0xFFFFEFE1)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(22.w)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.22),
|
||||
blurRadius: 18.w,
|
||||
offset: Offset(0, -4.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 42.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD6B28A).withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(2.w),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: SCDebounceWidget(
|
||||
onTap: () => SmartDialog.dismiss(tag: dialogTag),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
color: const Color(0xFF8C4A33),
|
||||
size: 24.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
_RoomRedPacketAssets.amountIcon,
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Text(
|
||||
localizations.roomRedPacketInsufficientBalance,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF8D2F2A),
|
||||
fontSize: 20.sp,
|
||||
height: 1.18,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
localizations.insufhcientGoldsGoToRecharge,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF9B604C),
|
||||
fontSize: 13.sp,
|
||||
height: 1.35,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 46.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(14.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF2D2A2),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
_RoomRedPacketAssets.amountIcon,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations.wallet,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF7B4133),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatSheetNumber(profileManager.myBalance.round()),
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFE33C35),
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
SCDebounceWidget(
|
||||
onTap: () => _goRecharge(context),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 50.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFF4741), Color(0xFFE91E26)],
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
border: Border.all(color: const Color(0xFFFFD282), width: 1.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFE22D25).withValues(alpha: 0.28),
|
||||
blurRadius: 12.w,
|
||||
offset: Offset(0, 4.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
localizations.goToRecharge,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _goRecharge(BuildContext context) {
|
||||
final routeContext = navigatorKey.currentState?.context ?? context;
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
SCNavigatorUtils.push(
|
||||
routeContext,
|
||||
WalletRoute.recharge,
|
||||
replace: false,
|
||||
).then((_) {
|
||||
if (!routeContext.mounted) {
|
||||
return;
|
||||
}
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
routeContext,
|
||||
listen: false,
|
||||
).balance();
|
||||
});
|
||||
}
|
||||
|
||||
static String _formatSheetNumber(int value) {
|
||||
final raw = value.toString();
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
if (i > 0 && (raw.length - i) % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(raw[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketAssets {
|
||||
static const String _base = 'sc_images/room/red_packet';
|
||||
static const String sheetBg = '$_base/sheet_bg.png';
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart';
|
||||
import '../../../app/routes/sc_fluro_navigator.dart';
|
||||
import '../../../modules/index/main_route.dart';
|
||||
import '../../../modules/room/voice_room_route.dart';
|
||||
@ -39,8 +39,8 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
static const int _menuReport = 5;
|
||||
static const int _menuBackground = 6;
|
||||
static const int _menuMusic = 7;
|
||||
static const int _menuRedPacket = 8;
|
||||
static bool get _showBackgroundFeature => true;
|
||||
static const int _menuRedPacket = 8;
|
||||
static bool get _showBackgroundFeature => true;
|
||||
|
||||
List<RoomMenu> items2 = [];
|
||||
|
||||
@ -96,13 +96,13 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
"sc_icon_room_msg_pic.png",
|
||||
),
|
||||
);
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuRedPacket,
|
||||
SCAppLocalizations.of(context)!.redEnvelope,
|
||||
"red_packet/menu_red_packet_icon.png",
|
||||
),
|
||||
);
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuRedPacket,
|
||||
SCAppLocalizations.of(context)!.redEnvelope,
|
||||
"red_packet/small_packet_icon.png",
|
||||
),
|
||||
);
|
||||
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
@ -272,9 +272,9 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
}, neeCrop: false);
|
||||
}
|
||||
} else if (item.id == _menuRedPacket) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
RoomRedPacketSendPanel.show(navigatorKey.currentState!.context);
|
||||
} else if (item.id == _menuRedPacket) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
RoomRedPacketSendPanel.show(navigatorKey.currentState!.context);
|
||||
} else if (item.id == _menuSettings) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
VoiceRoomRoute.openRoomEdit(
|
||||
@ -306,7 +306,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
}
|
||||
|
||||
Widget _buildMenuIcon(RoomMenu item) {
|
||||
if (item.id != _menuMusic && item.id != _menuRedPacket) {
|
||||
if (item.id != _menuMusic && item.id != _menuRedPacket) {
|
||||
return Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 45.w,
|
||||
@ -323,8 +323,8 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: item.id == _menuRedPacket ? 24.w : 26.w,
|
||||
height: item.id == _menuRedPacket ? 28.w : 26.w,
|
||||
width: item.id == _menuRedPacket ? 32.w : 26.w,
|
||||
height: item.id == _menuRedPacket ? 32.w : 26.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
@ -20,6 +19,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
@ -31,6 +31,14 @@ import '../../components/sc_compontent.dart';
|
||||
import '../../components/sc_tts.dart';
|
||||
import '../../components/text/sc_text.dart';
|
||||
|
||||
const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
);
|
||||
const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2);
|
||||
|
||||
///消息item
|
||||
class MsgItem extends StatefulWidget {
|
||||
final Function(SocialChatUserProfile? user) onClick;
|
||||
@ -418,6 +426,8 @@ class _MsgItemState extends State<MsgItem> {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
)
|
||||
: _buildStaticVipBubbleImage(
|
||||
@ -441,6 +451,8 @@ class _MsgItemState extends State<MsgItem> {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
@ -463,22 +475,13 @@ class _MsgItemState extends State<MsgItem> {
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
}
|
||||
|
||||
return Image(
|
||||
image: buildCachedImageProvider(
|
||||
imageUrl,
|
||||
logicalWidth: width,
|
||||
logicalHeight: height,
|
||||
),
|
||||
return SCNinePatchImage(
|
||||
url: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint(
|
||||
'[VIP][RoomChatBubble] image load failed url=$imageUrl error=$error',
|
||||
);
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
},
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback ?? SizedBox(width: width, height: height),
|
||||
);
|
||||
}
|
||||
|
||||
@ -495,7 +498,7 @@ class _MsgItemState extends State<MsgItem> {
|
||||
}
|
||||
|
||||
String _resolveRoomChatBubbleImageUrl(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
final candidates = <String?>[
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.roomNotOpenedUrl,
|
||||
@ -506,6 +509,10 @@ class _MsgItemState extends State<MsgItem> {
|
||||
resource?.cover,
|
||||
resource?.expand,
|
||||
resource?.sourceUrl,
|
||||
];
|
||||
return _firstNonBlank([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -86,31 +86,18 @@ class _RoomRedPacketFloatingEntry extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
width: 46.w,
|
||||
height: 46.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.black.withValues(alpha: 0.22),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD56A),
|
||||
width: 1.3.w,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 32.w,
|
||||
height: 32.w,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
if (snapshot.packets.length > 1)
|
||||
PositionedDirectional(
|
||||
|
||||
@ -18,6 +18,8 @@ class SCNetworkSvgaWidget extends StatefulWidget {
|
||||
this.fit = BoxFit.contain,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.allowDrawingOverflow = false,
|
||||
this.centerSliceRatio,
|
||||
this.sourceEdgeInset = EdgeInsets.zero,
|
||||
this.fallback,
|
||||
this.dynamicIdentity,
|
||||
this.movieConfigurer,
|
||||
@ -31,6 +33,8 @@ class SCNetworkSvgaWidget extends StatefulWidget {
|
||||
final BoxFit fit;
|
||||
final FilterQuality filterQuality;
|
||||
final bool allowDrawingOverflow;
|
||||
final EdgeInsets? centerSliceRatio;
|
||||
final EdgeInsets sourceEdgeInset;
|
||||
final Widget? fallback;
|
||||
final Object? dynamicIdentity;
|
||||
final SCNetworkSvgaMovieConfigurer? movieConfigurer;
|
||||
@ -147,11 +151,14 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoItem = _controller.videoItem;
|
||||
if (_hasError ||
|
||||
_controller.videoItem == null ||
|
||||
videoItem == null ||
|
||||
_loadedResource != widget.resource.trim()) {
|
||||
return _buildFallback();
|
||||
}
|
||||
final ninePatchSourceRect = _buildNinePatchSourceRect(videoItem);
|
||||
final centerSlice = _buildCenterSlice(ninePatchSourceRect);
|
||||
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
@ -163,6 +170,8 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
clearsAfterStop: false,
|
||||
filterQuality: widget.filterQuality,
|
||||
allowDrawingOverflow: widget.allowDrawingOverflow,
|
||||
centerSlice: centerSlice,
|
||||
ninePatchSourceRect: centerSlice == null ? null : ninePatchSourceRect,
|
||||
preferredSize:
|
||||
widget.width != null && widget.height != null
|
||||
? Size(widget.width!, widget.height!)
|
||||
@ -176,4 +185,54 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
return widget.fallback ??
|
||||
SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
|
||||
Rect _buildNinePatchSourceRect(MovieEntity movieEntity) {
|
||||
final imageWidth = movieEntity.params.viewBoxWidth;
|
||||
final imageHeight = movieEntity.params.viewBoxHeight;
|
||||
final left =
|
||||
widget.sourceEdgeInset.left.clamp(0.0, imageWidth / 4).toDouble();
|
||||
final top =
|
||||
widget.sourceEdgeInset.top.clamp(0.0, imageHeight / 4).toDouble();
|
||||
final right =
|
||||
widget.sourceEdgeInset.right.clamp(0.0, imageWidth / 4).toDouble();
|
||||
final bottom =
|
||||
widget.sourceEdgeInset.bottom.clamp(0.0, imageHeight / 4).toDouble();
|
||||
return Rect.fromLTRB(left, top, imageWidth - right, imageHeight - bottom);
|
||||
}
|
||||
|
||||
Rect? _buildCenterSlice(Rect sourceRect) {
|
||||
final ratio = widget.centerSliceRatio;
|
||||
if (ratio == null || sourceRect.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final imageWidth = sourceRect.width;
|
||||
final imageHeight = sourceRect.height;
|
||||
if (imageWidth <= 3.0 || imageHeight <= 3.0) {
|
||||
return null;
|
||||
}
|
||||
final left =
|
||||
(sourceRect.left + imageWidth * ratio.left)
|
||||
.clamp(sourceRect.left + 1.0, sourceRect.right - 2.0)
|
||||
.toDouble();
|
||||
final top =
|
||||
(sourceRect.top + imageHeight * ratio.top)
|
||||
.clamp(sourceRect.top + 1.0, sourceRect.bottom - 2.0)
|
||||
.toDouble();
|
||||
final rightInset =
|
||||
(imageWidth * ratio.right)
|
||||
.clamp(1.0, imageWidth - (left - sourceRect.left) - 1.0)
|
||||
.toDouble();
|
||||
final bottomInset =
|
||||
(imageHeight * ratio.bottom)
|
||||
.clamp(1.0, imageHeight - (top - sourceRect.top) - 1.0)
|
||||
.toDouble();
|
||||
final rect = Rect.fromLTRB(
|
||||
left,
|
||||
top,
|
||||
sourceRect.right - rightInset,
|
||||
sourceRect.bottom - bottomInset,
|
||||
);
|
||||
return rect.isEmpty ? null : rect;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ class _SVGAPainter extends CustomPainter {
|
||||
int get currentFrame => controller.currentFrame;
|
||||
MovieEntity get videoItem => controller.videoItem!;
|
||||
final FilterQuality filterQuality;
|
||||
final Rect? centerSlice;
|
||||
final Rect? ninePatchSourceRect;
|
||||
|
||||
/// Guaranteed to draw within the canvas bounds
|
||||
final bool clipRect;
|
||||
@ -13,6 +15,8 @@ class _SVGAPainter extends CustomPainter {
|
||||
this.controller, {
|
||||
this.fit = BoxFit.contain,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.centerSlice,
|
||||
this.ninePatchSourceRect,
|
||||
this.clipRect = true,
|
||||
}) : assert(
|
||||
controller.videoItem != null, 'Invalid SVGAAnimationController!'),
|
||||
@ -33,8 +37,145 @@ class _SVGAPainter extends CustomPainter {
|
||||
try {
|
||||
final canvasRect = Offset.zero & size;
|
||||
if (clipRect) canvas.clipRect(canvasRect);
|
||||
scaleCanvasToViewBox(canvas, canvasRect, Offset.zero & viewBoxSize);
|
||||
drawSprites(canvas, size);
|
||||
final slice = centerSlice;
|
||||
if (slice != null && fit == BoxFit.fill) {
|
||||
drawNinePatch(canvas, canvasRect, Offset.zero & viewBoxSize, slice);
|
||||
} else {
|
||||
scaleCanvasToViewBox(canvas, canvasRect, Offset.zero & viewBoxSize);
|
||||
drawSprites(canvas, size);
|
||||
}
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void drawNinePatch(
|
||||
Canvas canvas,
|
||||
Rect canvasRect,
|
||||
Rect viewBoxRect,
|
||||
Rect rawCenterSlice,
|
||||
) {
|
||||
final sourceRect = _validSourceRect(viewBoxRect);
|
||||
if (sourceRect == null) {
|
||||
scaleCanvasToViewBox(canvas, canvasRect, viewBoxRect);
|
||||
drawSprites(canvas, canvasRect.size);
|
||||
return;
|
||||
}
|
||||
|
||||
final centerSlice = _validCenterSlice(sourceRect, rawCenterSlice);
|
||||
if (centerSlice == null) {
|
||||
scaleCanvasToViewBox(canvas, canvasRect, viewBoxRect);
|
||||
drawSprites(canvas, canvasRect.size);
|
||||
return;
|
||||
}
|
||||
|
||||
final left = centerSlice.left - sourceRect.left;
|
||||
final right = sourceRect.right - centerSlice.right;
|
||||
final top = centerSlice.top - sourceRect.top;
|
||||
final bottom = sourceRect.bottom - centerSlice.bottom;
|
||||
|
||||
final horizontalBorder = left + right;
|
||||
final verticalBorder = top + bottom;
|
||||
final horizontalScale = horizontalBorder <= 0
|
||||
? 1.0
|
||||
: min(1.0, canvasRect.width / horizontalBorder);
|
||||
final verticalScale = verticalBorder <= 0
|
||||
? 1.0
|
||||
: min(1.0, canvasRect.height / verticalBorder);
|
||||
|
||||
final dstLeft = left * horizontalScale;
|
||||
final dstRight = right * horizontalScale;
|
||||
final dstTop = top * verticalScale;
|
||||
final dstBottom = bottom * verticalScale;
|
||||
|
||||
final sourceXs = <double>[
|
||||
sourceRect.left,
|
||||
centerSlice.left,
|
||||
centerSlice.right,
|
||||
sourceRect.right,
|
||||
];
|
||||
final sourceYs = <double>[
|
||||
sourceRect.top,
|
||||
centerSlice.top,
|
||||
centerSlice.bottom,
|
||||
sourceRect.bottom,
|
||||
];
|
||||
final destXs = <double>[
|
||||
canvasRect.left,
|
||||
canvasRect.left + dstLeft,
|
||||
canvasRect.right - dstRight,
|
||||
canvasRect.right,
|
||||
];
|
||||
final destYs = <double>[
|
||||
canvasRect.top,
|
||||
canvasRect.top + dstTop,
|
||||
canvasRect.bottom - dstBottom,
|
||||
canvasRect.bottom,
|
||||
];
|
||||
|
||||
for (var row = 0; row < 3; row++) {
|
||||
for (var column = 0; column < 3; column++) {
|
||||
final src = Rect.fromLTRB(
|
||||
sourceXs[column],
|
||||
sourceYs[row],
|
||||
sourceXs[column + 1],
|
||||
sourceYs[row + 1],
|
||||
);
|
||||
final dst = Rect.fromLTRB(
|
||||
destXs[column],
|
||||
destYs[row],
|
||||
destXs[column + 1],
|
||||
destYs[row + 1],
|
||||
);
|
||||
if (src.width <= 0 ||
|
||||
src.height <= 0 ||
|
||||
dst.width <= 0 ||
|
||||
dst.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
_drawSourceSegment(canvas, src, dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rect? _validSourceRect(Rect viewBoxRect) {
|
||||
final rect = ninePatchSourceRect ?? viewBoxRect;
|
||||
final left = rect.left
|
||||
.clamp(viewBoxRect.left, viewBoxRect.right)
|
||||
.toDouble();
|
||||
final top = rect.top.clamp(viewBoxRect.top, viewBoxRect.bottom).toDouble();
|
||||
final right = rect.right.clamp(left, viewBoxRect.right).toDouble();
|
||||
final bottom = rect.bottom.clamp(top, viewBoxRect.bottom).toDouble();
|
||||
final result = Rect.fromLTRB(left, top, right, bottom);
|
||||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
Rect? _validCenterSlice(Rect sourceRect, Rect rawCenterSlice) {
|
||||
final left = rawCenterSlice.left
|
||||
.clamp(sourceRect.left, sourceRect.right)
|
||||
.toDouble();
|
||||
final top = rawCenterSlice.top
|
||||
.clamp(sourceRect.top, sourceRect.bottom)
|
||||
.toDouble();
|
||||
final right = rawCenterSlice.right.clamp(left, sourceRect.right).toDouble();
|
||||
final bottom = rawCenterSlice.bottom
|
||||
.clamp(top, sourceRect.bottom)
|
||||
.toDouble();
|
||||
final result = Rect.fromLTRB(left, top, right, bottom);
|
||||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
void _drawSourceSegment(Canvas canvas, Rect sourceRect, Rect destRect) {
|
||||
canvas.save();
|
||||
try {
|
||||
canvas.clipRect(destRect);
|
||||
canvas.translate(destRect.left, destRect.top);
|
||||
canvas.scale(
|
||||
destRect.width / sourceRect.width,
|
||||
destRect.height / sourceRect.height,
|
||||
);
|
||||
canvas.translate(-sourceRect.left, -sourceRect.top);
|
||||
drawSprites(canvas, destRect.size);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -36,6 +36,17 @@ class SVGAImage extends StatefulWidget {
|
||||
///
|
||||
/// Defaults to null.
|
||||
final Size? preferredSize;
|
||||
|
||||
/// Stretchable center area in the SVGA viewBox coordinate space.
|
||||
///
|
||||
/// When supplied with [ninePatchSourceRect], the painter renders the SVGA like
|
||||
/// a nine-patch image so borders and corners are not distorted.
|
||||
final Rect? centerSlice;
|
||||
|
||||
/// Source area in the SVGA viewBox coordinate space used for nine-patch
|
||||
/// drawing. Useful for skipping guide pixels around exported nine-patch art.
|
||||
final Rect? ninePatchSourceRect;
|
||||
|
||||
const SVGAImage(
|
||||
this._controller, {
|
||||
super.key,
|
||||
@ -44,6 +55,8 @@ class SVGAImage extends StatefulWidget {
|
||||
this.allowDrawingOverflow,
|
||||
this.clearsAfterStop = true,
|
||||
this.preferredSize,
|
||||
this.centerSlice,
|
||||
this.ninePatchSourceRect,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -285,6 +298,8 @@ class _SVGAImageState extends State<SVGAImage> {
|
||||
widget._controller,
|
||||
fit: widget.fit,
|
||||
filterQuality: widget.filterQuality,
|
||||
centerSlice: widget.centerSlice,
|
||||
ninePatchSourceRect: widget.ninePatchSourceRect,
|
||||
// default is allowing overflow for backward compatibility
|
||||
clipRect: widget.allowDrawingOverflow == false,
|
||||
),
|
||||
|
||||
6
需求进度.md
6
需求进度.md
@ -37,7 +37,8 @@
|
||||
- 第十步已为 Gift Wall 详情页补齐礼物名称/金额解析链路:礼物墙接口返回后会再用全局 `/gift/list` 配置表按 `standardId / id / giftCode / giftPhoto` 做二次匹配,补齐缺失的 `giftName / giftCandy / giftIntegral / giftPhoto`,用于单个礼物卡片和 `Total Value` 统计。
|
||||
- 第十一步已补齐 Gift Wall 礼物动效预览:礼物卡点击时如果礼物 `giftSourceUrl` 是 `.svga`,会在当前页面全屏播放一次 SVGA 动效;播放期间使用页面状态锁防止重复叠加,播放完成或加载失败后自动清理 overlay 状态。
|
||||
- 第十二步已补 Wall Of Honors 独立页面骨架:个人主页 `Wall Of Honors` 标题点击进入 `/main/person/wallOfHonors`;新页面先按无数据状态只展示满宽顶部返回栏、`My Achievements(0)` 标题、`Honors(0) / Event(0)` 两个 tab 和底部空态文案,顶部栏沿用满宽 `Stack` 写法避免返回箭头挤压标题。
|
||||
- 验证进度:`dart format lib/modules/user/profile/profile_wall_of_honors_page.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/profile_wall_of_honors_page.dart lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 无 error/warning,仅保留旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。
|
||||
- 第十三步已把个人主页里的墙模块改为预览态:`Gift Wall` 只保留前 9 个礼物用于主页预览,`Wall Of Honors` 当前无数据时也固定展示 9 个占位;进入独立 `Gift Wall / Wall Of Honors` 页面后再走详情页完整数据/页面逻辑。
|
||||
- 验证进度:`dart format lib/modules/user/profile/person_detail_page.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/person_detail_page.dart lib/modules/user/profile/profile_wall_of_honors_page.dart lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart lib/modules/index/main_route.dart` 无 error/warning,仅保留旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。
|
||||
|
||||
## 本轮 Android 语言房音乐功能(进行中)
|
||||
- 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。
|
||||
@ -546,6 +547,9 @@
|
||||
- 已修正红包历史页和领取记录页的 RTL 排版:列表主文本列改回 `start` 对齐以跟随 RTL 靠右,金额列跟随 `end` 靠外侧;金额和时间强制按 LTR 渲染,避免阿语环境下时间顺序错乱或负号跑到数字后面。
|
||||
- 已按最新接口文档接入红包领取记录新接口:领取记录页优先调用 `GET /go/app/voice-room/red-packet/claim-records?packetNo=...` 拉取领取人列表,失败时再兜底旧 detail records;同时历史页和领取记录页跳转改为深色无默认转场 route,避免进入页面时先闪一下白屏。
|
||||
- 已将红包规则页替换为正式规则文案,并同步更新 `en/ar/bn/tr` 多语言;第 4 条每日房间红包发送上限不再写死,进入规则页时调用红包配置接口读取 `dailySendLimit` 后填入 `%1$s` 占位。
|
||||
- 已补齐红包领取记录页的发包摘要:从发红包记录页点击进入后,顶部固定展示该红包图标、红包数量、发送时间、发出金额和退款金额;即使领取列表为空仍展示红包金额,空态只作为领取人列表区域提示。
|
||||
- 已新增红包发送余额不足充值引导:发送接口返回 `voice_room_red_packet_insufficient_balance` 时关闭红包发送面板,改为弹出约半屏高度的底部充值弹窗,展示当前金币余额和充值按钮;点击充值复用 `me/wallet` 的 `WalletRoute.recharge` 进入充值页,返回后刷新用户余额。
|
||||
- 已按反馈统一红包入口图标:房间右侧待领红包入口去掉黑底和金色外圈,直接展示放大的红包图标,尺寸与游戏入口接近;房间菜单内红包项也从白色线性图标改为同一张红包图标。
|
||||
|
||||
## 已知问题
|
||||
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user