vip接口 以及房间兜底

This commit is contained in:
roxy 2026-04-29 20:37:03 +08:00
parent 9f4df82c42
commit fd445ccdaa
9 changed files with 1832 additions and 1008 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -15,8 +14,10 @@ import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import '../../../../shared/data_sources/models/enum/sc_level_type.dart'; import '../../../../shared/data_sources/models/enum/sc_level_type.dart';
class UserLevelPage extends StatefulWidget { class UserLevelPage extends StatefulWidget {
const UserLevelPage({super.key});
@override @override
_UserLevelPageState createState() => _UserLevelPageState(); State<UserLevelPage> createState() => _UserLevelPageState();
} }
class _UserLevelPageState extends State<UserLevelPage> { class _UserLevelPageState extends State<UserLevelPage> {
@ -78,26 +79,26 @@ class _UserLevelPageState extends State<UserLevelPage> {
maxWidth: 115.w, maxWidth: 115.w,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
AccountStorage() AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.userNickname ?? ?.userNickname ??
"", "",
fontSize: 15.sp, fontSize: 15.sp,
type: type:
AccountStorage() AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.getVIP() ?.getVIP()
?.name ?? ?.name ??
"", "",
needScroll: needScroll:
(AccountStorage() (AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.userNickname ?.userNickname
?.characters ?.characters
.length ?? .length ??
0) > 0) >
10, 10,
), ),
SizedBox(width: 3.w), SizedBox(width: 3.w),
@ -162,7 +163,6 @@ class _UserLevelPageState extends State<UserLevelPage> {
textColor: Colors.white, textColor: Colors.white,
), ),
Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"), Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"),
Image.asset("sc_images/level/sc_icon_user_level_center_bg_2.png"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
fontSize: 20.sp, fontSize: 20.sp,
@ -171,7 +171,9 @@ class _UserLevelPageState extends State<UserLevelPage> {
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of(context)!.spendCoinsToGainExperiencePoints, SCAppLocalizations.of(
context,
)!.spendCoinsToGainExperiencePoints,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textColor: Colors.white, textColor: Colors.white,
@ -196,7 +198,7 @@ class _UserLevelPageState extends State<UserLevelPage> {
SCNavigatorUtils.push(context, StoreRoute.list); SCNavigatorUtils.push(context, StoreRoute.list);
}, },
), ),
SizedBox(height: 20.w,) SizedBox(height: 20.w),
], ],
), ),
) )

View File

@ -1,7 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:marquee/marquee.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart';
@ -16,8 +14,10 @@ import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import '../../../../shared/data_sources/models/enum/sc_level_type.dart'; import '../../../../shared/data_sources/models/enum/sc_level_type.dart';
class WealthLevelPage extends StatefulWidget { class WealthLevelPage extends StatefulWidget {
const WealthLevelPage({super.key});
@override @override
_WealthLevelPageState createState() => _WealthLevelPageState(); State<WealthLevelPage> createState() => _WealthLevelPageState();
} }
class _WealthLevelPageState extends State<WealthLevelPage> { class _WealthLevelPageState extends State<WealthLevelPage> {
@ -79,26 +79,26 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
maxWidth: 125.w, maxWidth: 125.w,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
AccountStorage() AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.userNickname ?? ?.userNickname ??
"", "",
fontSize: 15.sp, fontSize: 15.sp,
type: type:
AccountStorage() AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.getVIP() ?.getVIP()
?.name ?? ?.name ??
"", "",
needScroll: needScroll:
(AccountStorage() (AccountStorage()
.getCurrentUser() .getCurrentUser()
?.userProfile ?.userProfile
?.userNickname ?.userNickname
?.characters ?.characters
.length ?? .length ??
0) > 0) >
10, 10,
), ),
SizedBox(width: 3.w), SizedBox(width: 3.w),
@ -163,16 +163,17 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
textColor: Colors.white, textColor: Colors.white,
), ),
Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"), Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"),
Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_2.png"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
fontSize: 20.sp, fontSize: 20.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
textColor:SocialChatTheme.primaryLight, textColor: SocialChatTheme.primaryLight,
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of(context)!.spendCoinsToGainExperiencePoints, SCAppLocalizations.of(
context,
)!.spendCoinsToGainExperiencePoints,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textColor: Colors.white, textColor: Colors.white,
@ -197,7 +198,7 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
SCNavigatorUtils.push(context, StoreRoute.list); SCNavigatorUtils.push(context, StoreRoute.list);
}, },
), ),
SizedBox(height: 20.w,) SizedBox(height: 20.w),
], ],
), ),
) )

View File

@ -12,8 +12,10 @@ import 'package:yumi/modules/user/vip/vip_route.dart';
import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
@ -32,6 +34,8 @@ class _MePage2State extends State<MePage2> {
static const double _inviteActivityHorizontalScale = 750 / 662; static const double _inviteActivityHorizontalScale = 750 / 662;
Map<String, SCUserCounterRes> _counterMap = {}; Map<String, SCUserCounterRes> _counterMap = {};
SCVipStatusRes? _vipStatus;
bool _isVipStatusLoading = false;
@override @override
void initState() { void initState() {
@ -49,6 +53,7 @@ class _MePage2State extends State<MePage2> {
profileManager.fetchUserProfileData(); profileManager.fetchUserProfileData();
profileManager.balance(); profileManager.balance();
_loadCounter(); _loadCounter();
_loadVipStatus();
} }
Future<void> _loadCounter() async { Future<void> _loadCounter() async {
@ -68,6 +73,47 @@ class _MePage2State extends State<MePage2> {
} catch (_) {} } catch (_) {}
} }
Future<void> _loadVipStatus() async {
if (mounted) {
setState(() => _isVipStatusLoading = true);
}
try {
final status = await _loadVipEntryStatus();
debugPrint(
'[VIP][MeEntry] status loaded active=${status.active} '
'level=${status.level} levelCode=${status.levelCode} '
'expireAt=${status.expireAt} '
'badge=${_vipEntryBadgeLogValue(status.badge)}',
);
if (!mounted) return;
setState(() {
_vipStatus = status;
_isVipStatusLoading = false;
});
} catch (error) {
debugPrint('[VIP][MeEntry] status load failed error=$error');
if (!mounted) return;
setState(() => _isVipStatusLoading = false);
}
}
Future<SCVipStatusRes> _loadVipEntryStatus() async {
final repository = SCVipRepositoryImp();
try {
final home = await repository.vipHome();
final state = home.state;
if (state != null && state.levelInt > 0) {
debugPrint('[VIP][MeEntry] use vip home state');
return state;
}
debugPrint('[VIP][MeEntry] vip home state empty, fallback status');
} catch (error) {
debugPrint('[VIP][MeEntry] vip home load failed error=$error');
}
return repository.vipStatus();
}
void _openInviteActivity() { void _openInviteActivity() {
final inviteActivityUrl = _appendToken(_inviteActivityUrl); final inviteActivityUrl = _appendToken(_inviteActivityUrl);
if (inviteActivityUrl.isEmpty) return; if (inviteActivityUrl.isEmpty) return;
@ -108,7 +154,7 @@ class _MePage2State extends State<MePage2> {
SizedBox(height: 12.w), SizedBox(height: 12.w),
_buildEntryRow(), _buildEntryRow(),
SizedBox(height: 12.w), SizedBox(height: 12.w),
_buildWalletVipRow(), _buildWalletVipRow(profileManager),
_buildInviteActivityBanner(), _buildInviteActivityBanner(),
_buildMenuCard2(), _buildMenuCard2(),
SizedBox(height: 12.w), SizedBox(height: 12.w),
@ -268,10 +314,10 @@ class _MePage2State extends State<MePage2> {
); );
} }
Widget _buildWalletVipRow() { Widget _buildWalletVipRow(SocialChatUserProfileManager profileManager) {
return Row( return Row(
children: [ children: [
Expanded(child: _buildVipEntryCard()), Expanded(child: _buildVipEntryCard(profileManager)),
SizedBox(width: 10.w), SizedBox(width: 10.w),
Expanded(child: _buildWalletCard()), Expanded(child: _buildWalletCard()),
], ],
@ -354,9 +400,17 @@ class _MePage2State extends State<MePage2> {
); );
} }
Widget _buildVipEntryCard() { Widget _buildVipEntryCard(SocialChatUserProfileManager profileManager) {
final vipLevel = _vipEntryLevel(profileManager);
final isActiveVip = vipLevel > 0;
final badgeResource = isActiveVip ? _vipStatus?.badge : null;
return SCDebounceWidget( return SCDebounceWidget(
onTap: () => SCNavigatorUtils.push(context, VipRoute.detail), onTap: () async {
await SCNavigatorUtils.push(context, VipRoute.detail);
if (mounted) {
_loadVipStatus();
}
},
child: Container( child: Container(
height: 68.w, height: 68.w,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -394,16 +448,38 @@ class _MePage2State extends State<MePage2> {
PositionedDirectional( PositionedDirectional(
start: 13.w, start: 13.w,
bottom: 10.w, bottom: 10.w,
child: _buildVipLevelMark(5), child:
_isVipStatusLoading && _vipStatus == null
? SizedBox(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
color: const Color(0xFFFFD155),
strokeWidth: 2.w,
),
)
: isActiveVip
? _buildVipLevelMark(vipLevel)
: Text(
'Not activated',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(
0xFFFFE8A7,
).withValues(alpha: 0.82),
fontSize: 11.sp,
fontWeight: FontWeight.w600,
height: 1,
),
),
), ),
PositionedDirectional( PositionedDirectional(
end: 2.w, end: 2.w,
top: 8.w, top: 8.w,
child: Image.asset( child: Opacity(
'sc_images/vip/sc_vip_badge_5.png', opacity: isActiveVip ? 1 : 0.52,
width: 52.w, child: _buildVipEntryBadge(resource: badgeResource),
height: 52.w,
fit: BoxFit.contain,
), ),
), ),
], ],
@ -412,6 +488,64 @@ class _MePage2State extends State<MePage2> {
); );
} }
Widget _buildVipEntryBadge({required SCVipResourceRes? resource}) {
final empty = SizedBox(width: 52.w, height: 52.w);
final url = resource?.previewUrl?.trim();
if (url == null || url.isEmpty) {
return empty;
}
return netImage(
url: url,
width: 52.w,
height: 52.w,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget: empty,
);
}
String _vipEntryBadgeLogValue(SCVipResourceRes? resource) {
if (resource == null) {
return 'null';
}
return '{id:${resource.resourceId},name:${resource.name},'
'hasPreviewUrl:${resource.hasPreviewUrl},'
'previewUrl:${resource.previewUrl}}';
}
int _vipEntryLevel(SocialChatUserProfileManager profileManager) {
final status = _vipStatus;
if (status != null && status.isActive && status.levelInt > 0) {
return status.levelInt.clamp(1, 5).toInt();
}
final vipResource = profileManager.currentUserProfile?.getVIP();
return _parseVipLevel(vipResource?.name) ??
_parseVipLevel(vipResource?.code) ??
0;
}
int? _parseVipLevel(String? value) {
final text = value?.trim();
if (text == null || text.isEmpty) {
return null;
}
final vipMatch = RegExp(
r'VIP\s*([1-5])',
caseSensitive: false,
).firstMatch(text);
if (vipMatch != null) {
return int.tryParse(vipMatch.group(1) ?? '');
}
final trailingLevelMatch = RegExp(r'([1-5])$').firstMatch(text);
if (trailingLevelMatch != null) {
return int.tryParse(trailingLevelMatch.group(1) ?? '');
}
return null;
}
Widget _buildVipLevelMark(int level) { Widget _buildVipLevelMark(int level) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -10,6 +10,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.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/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart';
@ -56,7 +57,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
]); ]);
final home = results[0] as SCVipHomeRes; final home = results[0] as SCVipHomeRes;
final status = results[1] as SCVipStatusRes; final status = results[1] as SCVipStatusRes;
final resolvedStatus = status.levelInt > 0 ? status : home.state; final resolvedStatus = _resolveStatus(status, home.state);
final selectedLevel = _resolveInitialLevel(home, resolvedStatus); final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
_logLoadedVipData(home, resolvedStatus, selectedLevel); _logLoadedVipData(home, resolvedStatus, selectedLevel);
@ -80,6 +81,18 @@ class _VipDetailPageState extends State<VipDetailPage> {
} }
} }
SCVipStatusRes? _resolveStatus(
SCVipStatusRes status,
SCVipStatusRes? homeState,
) {
if (homeState != null && homeState.levelInt > 0) {
if (status.levelInt <= 0 || status.levelInt == homeState.levelInt) {
return homeState;
}
}
return status.levelInt > 0 ? status : homeState;
}
int _resolveInitialLevel(SCVipHomeRes home, SCVipStatusRes? status) { int _resolveInitialLevel(SCVipHomeRes home, SCVipStatusRes? status) {
if (status?.isActive == true && status!.levelInt > 0) { if (status?.isActive == true && status!.levelInt > 0) {
return status.levelInt.clamp(1, 5).toInt(); return status.levelInt.clamp(1, 5).toInt();
@ -100,8 +113,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
return 1; return 1;
} }
SCVipLevelConfigRes? _levelConfigFor(int level) { SCVipLevelConfigRes? _levelConfigFromHome(SCVipHomeRes? home, int level) {
final levels = _home?.levels ?? <SCVipLevelConfigRes>[]; final levels = home?.levels ?? <SCVipLevelConfigRes>[];
for (final config in levels) { for (final config in levels) {
if (config.levelInt == level) { if (config.levelInt == level) {
return config; return config;
@ -110,11 +123,30 @@ class _VipDetailPageState extends State<VipDetailPage> {
return null; return null;
} }
SCVipLevelConfigRes? _levelConfigFor(int level) =>
_levelConfigFromHome(_home, level);
SCVipLevelConfigRes? get _selectedLevelConfig =>
_levelConfigFor(_selectedLevel);
SCVipStatusRes? get _selectedLevelState {
final status = _status;
if (status != null && status.levelInt == _selectedLevel) {
return status;
}
return null;
}
void _selectLevel(int level) { void _selectLevel(int level) {
setState(() { setState(() {
_selectedLevel = level; _selectedLevel = level;
_preview = null; _preview = null;
}); });
_logSelectedLevelResources(
level,
config: _levelConfigFor(level),
status: _status,
);
_loadPreviewForLevel(level); _loadPreviewForLevel(level);
} }
@ -216,6 +248,58 @@ class _VipDetailPageState extends State<VipDetailPage> {
'frontend has no level data to render.', 'frontend has no level data to render.',
); );
} }
_logSelectedLevelResources(
selectedLevel,
config: _levelConfigFromHome(home, selectedLevel),
status: status,
);
}
void _logSelectedLevelResources(
int level, {
required SCVipLevelConfigRes? config,
required SCVipStatusRes? status,
}) {
final state = status?.levelInt == level ? status : null;
if (config == null && state == null) {
debugPrint(
'[VIP][Page][Warn] selected level=$level has no config or state',
);
return;
}
final badge = _preferResource(config?.badge, state?.badge);
final avatarFrame = _preferResource(
config?.avatarFrame,
state?.avatarFrame,
);
final entryEffect = _preferResource(
config?.entryEffect,
state?.entryEffect,
);
final chatBubble = _preferResource(config?.chatBubble, state?.chatBubble);
final floatPicture = _preferResource(
config?.floatPicture,
state?.floatPicture,
);
debugPrint(
'[VIP][Page] selected level resources level=$level '
'badge=${_resourceLogValue(badge)} '
'avatarFrame=${_resourceLogValue(avatarFrame)} '
'entryEffect=${_resourceLogValue(entryEffect)} '
'chatBubble=${_resourceLogValue(chatBubble)} '
'floatPicture=${_resourceLogValue(floatPicture)}',
);
}
String _resourceLogValue(SCVipResourceRes? resource) {
if (!_hasResource(resource)) {
return 'null';
}
return '{id:${resource?.resourceId},name:${resource?.name},'
'hasPreviewUrl:${resource?.hasPreviewUrl},'
'hasSourceUrl:${resource?.hasSourceUrl},'
'previewUrl:${resource?.previewUrl},'
'sourceUrl:${resource?.sourceResourceUrl}}';
} }
@override @override
@ -331,6 +415,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
); );
} }
final decorationItems = _decorationPrivilegeItems;
final privilegeItems = _vipPrivilegeItems;
return SingleChildScrollView( return SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(12.w, 40.w, 12.w, 18.w), padding: EdgeInsets.fromLTRB(12.w, 40.w, 12.w, 18.w),
@ -340,13 +427,17 @@ class _VipDetailPageState extends State<VipDetailPage> {
SizedBox(height: 30.w), SizedBox(height: 30.w),
_buildLevelDivider(), _buildLevelDivider(),
SizedBox(height: 3.w), SizedBox(height: 3.w),
_buildSectionTitle('Decoration Privileges'), if (decorationItems.isNotEmpty) ...[
SizedBox(height: 10.w), _buildSectionTitle('Decoration Privileges'),
_buildDecorationGrid(), SizedBox(height: 10.w),
SizedBox(height: 20.w), _buildDecorationGrid(decorationItems),
_buildSectionTitle('VIP Privileges'), SizedBox(height: 20.w),
SizedBox(height: 14.w), ],
_buildPrivilegeGrid(), if (privilegeItems.isNotEmpty) ...[
_buildSectionTitle('VIP Privileges'),
SizedBox(height: 14.w),
_buildPrivilegeGrid(privilegeItems),
],
SizedBox(height: 76.w), SizedBox(height: 76.w),
], ],
), ),
@ -519,11 +610,11 @@ class _VipDetailPageState extends State<VipDetailPage> {
PositionedDirectional( PositionedDirectional(
end: 24.w, end: 24.w,
top: -36.w, top: -36.w,
child: Image.asset( child: _buildResourceImage(
'sc_images/vip/sc_vip_badge_$assetLevel.png', resource: _selectedBadge,
width: 135.w, width: 135.w,
height: 135.w, height: 135.w,
fit: BoxFit.contain, fallbackIcon: Icons.workspace_premium_outlined,
), ),
), ),
], ],
@ -601,17 +692,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
); );
} }
Widget _buildDecorationGrid() { Widget _buildDecorationGrid(List<_VipFeatureData> items) {
final items = [
_VipFeatureData(
'SVIP Badge',
'sc_images/vip/sc_vip_badge_$_assetLevel.png',
),
_VipFeatureData('Frame', null, Icons.person_pin_circle_outlined),
_VipFeatureData('Mount', null, Icons.auto_awesome_motion),
_VipFeatureData('Mic Animation', null, Icons.radio_button_checked),
_VipFeatureData('Entry Effect', null, Icons.airline_seat_recline_extra),
];
return Wrap( return Wrap(
spacing: 10.w, spacing: 10.w,
runSpacing: 10.w, runSpacing: 10.w,
@ -620,7 +701,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
.map( .map(
(item) => _buildFeatureTile( (item) => _buildFeatureTile(
item, item,
width: item.title == 'Entry Effect' ? 206.w : 98.w, width: item.wide ? 206.w : 98.w,
height: 100.w, height: 100.w,
), ),
) )
@ -628,63 +709,293 @@ class _VipDetailPageState extends State<VipDetailPage> {
); );
} }
Widget _buildPrivilegeGrid() { Widget _buildPrivilegeGrid(List<_VipFeatureData> items) {
final items = [
_VipFeatureData('Gif Profile\nPicture', null, Icons.badge_outlined),
_VipFeatureData('Colorful\nNickname', null, Icons.credit_card),
_VipFeatureData('Gif Room\nPicture', null, Icons.home_rounded),
_VipFeatureData('Colorful ID', null, Icons.pin_rounded),
];
return Wrap( return Wrap(
spacing: 34.w, spacing: 34.w,
runSpacing: 24.w, runSpacing: 24.w,
children: children:
items.asMap().entries.map((entry) { items.asMap().entries.map((entry) {
final item = entry.value; final item = entry.value;
return GestureDetector( return SizedBox(
behavior: HitTestBehavior.opaque, width: 74.w,
onTap: child: Column(
() => SCNavigatorUtils.push( children: [
context, Container(
'${VipRoute.benefit}?index=${entry.key}', width: 46.w,
height: 46.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.08),
),
alignment: Alignment.center,
child: _buildFeatureIcon(item, size: 23.w),
), ),
child: SizedBox( SizedBox(height: 7.w),
width: 74.w, Text(
child: Column( item.title,
children: [ textAlign: TextAlign.center,
Container( style: TextStyle(
width: 46.w, color: Colors.white,
height: 46.w, fontSize: 12.sp,
decoration: BoxDecoration( fontWeight: FontWeight.w400,
shape: BoxShape.circle, height: 1.1,
color: Colors.white.withValues(alpha: 0.08),
),
alignment: Alignment.center,
child: Icon(
item.icon,
color: const Color(0xFFFF8E3D),
size: 23.w,
),
), ),
SizedBox(height: 7.w), ),
Text( ],
item.title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w400,
height: 1.1,
),
),
],
),
), ),
); );
}).toList(), }).toList(),
); );
} }
SCVipResourceRes? get _selectedBadge => _selectedResource(
fromConfig: (config) => config.badge,
fromState: (status) => status.badge,
);
SCVipResourceRes? get _selectedAvatarFrame => _selectedResource(
fromConfig: (config) => config.avatarFrame,
fromState: (status) => status.avatarFrame,
);
SCVipResourceRes? get _selectedEntryEffect => _selectedResource(
fromConfig: (config) => config.entryEffect,
fromState: (status) => status.entryEffect,
);
SCVipResourceRes? get _selectedChatBubble => _selectedResource(
fromConfig: (config) => config.chatBubble,
fromState: (status) => status.chatBubble,
);
SCVipResourceRes? get _selectedFloatPicture => _selectedResource(
fromConfig: (config) => config.floatPicture,
fromState: (status) => status.floatPicture,
);
SCVipResourceRes? _selectedResource({
required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig,
required SCVipResourceRes? Function(SCVipStatusRes status) fromState,
}) {
final config = _selectedLevelConfig;
final state = _selectedLevelState;
return _preferResource(
config == null ? null : fromConfig(config),
state == null ? null : fromState(state),
);
}
SCVipResourceRes? _preferResource(
SCVipResourceRes? configResource,
SCVipResourceRes? stateResource,
) {
if (_hasResourceUrl(configResource) || !_hasResourceUrl(stateResource)) {
return _hasResource(configResource) ? configResource : stateResource;
}
return stateResource;
}
List<_VipFeatureData> get _decorationPrivilegeItems {
if (_selectedLevelConfig == null && _selectedLevelState == null) {
return const <_VipFeatureData>[];
}
final items = <_VipFeatureData>[];
_addResourceItem(
items,
resource: _selectedBadge,
fallbackTitle: 'VIP Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
items,
resource: _selectedAvatarFrame,
fallbackTitle: 'Avatar Frame',
fallbackIcon: Icons.person_pin_circle_outlined,
);
_addResourceItem(
items,
resource: _selectedChatBubble,
fallbackTitle: 'Chat Bubble',
fallbackIcon: Icons.chat_bubble_outline_rounded,
);
_addResourceItem(
items,
resource: _selectedFloatPicture,
fallbackTitle: 'Float Picture',
fallbackIcon: Icons.filter_frames_outlined,
);
_addResourceItem(
items,
resource: _selectedEntryEffect,
fallbackTitle: 'Entry Effect',
fallbackIcon: Icons.airline_seat_recline_extra,
wide: true,
);
return items;
}
List<_VipFeatureData> get _vipPrivilegeItems {
// The VIP home API currently returns resource perks only:
// badge/avatarFrame/entryEffect/chatBubble/floatPicture.
// Do not render demo-only functional privileges without backend data.
return const <_VipFeatureData>[];
}
void _addResourceItem(
List<_VipFeatureData> items, {
required SCVipResourceRes? resource,
required String fallbackTitle,
IconData? fallbackIcon,
bool wide = false,
}) {
if (!_hasResource(resource)) {
return;
}
items.add(
_VipFeatureData(
_resourceTitle(resource, fallbackTitle),
null,
icon: fallbackIcon,
resourceUrl: resource?.previewUrl,
wide: wide,
),
);
}
bool _hasResource(SCVipResourceRes? resource) {
return resource?.hasContent == true;
}
bool _hasResourceUrl(SCVipResourceRes? resource) {
return resource?.hasAnyUrl == true;
}
String _resourceTitle(SCVipResourceRes? resource, String fallbackTitle) {
final name = resource?.name?.trim();
if (name != null && name.isNotEmpty) {
return name;
}
return fallbackTitle;
}
Widget _buildResourceImage({
required SCVipResourceRes? resource,
required double width,
required double height,
required IconData fallbackIcon,
}) {
final url = resource?.previewUrl?.trim();
if (url != null && url.isNotEmpty) {
return netImage(
url: url,
width: width,
height: height,
fit: BoxFit.contain,
noDefaultImg: true,
loadingWidget: SizedBox(
width: width,
height: height,
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
),
errorWidget: _buildLargeResourcePlaceholder(
fallbackIcon,
width: width,
height: height,
),
);
}
if (!_hasResource(resource)) {
return SizedBox(width: width, height: height);
}
return _buildLargeResourcePlaceholder(
fallbackIcon,
width: width,
height: height,
);
}
Widget _buildLargeResourcePlaceholder(
IconData icon, {
required double width,
required double height,
}) {
return SizedBox(
width: width,
height: height,
child: Center(
child: Container(
width: (width * 0.48).clamp(46.w, 74.w).toDouble(),
height: (height * 0.48).clamp(46.w, 74.w).toDouble(),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withValues(alpha: 0.20),
border: Border.all(
color: const Color(0xFFFFD155).withValues(alpha: 0.45),
width: 1.w,
),
),
alignment: Alignment.center,
child: Icon(icon, color: const Color(0xFFFFD155), size: 32.w),
),
),
);
}
Widget _buildFeatureIcon(_VipFeatureData item, {required double size}) {
final url = item.resourceUrl?.trim();
if (url != null && url.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(6.w),
child: netImage(
url: url,
width: size,
height: size,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget: Icon(
item.icon ?? Icons.auto_awesome,
color: const Color(0xFFFF8E3D),
size: size,
),
),
);
}
return Icon(
item.icon ?? Icons.auto_awesome,
color: const Color(0xFFFF8E3D),
size: size,
);
}
Widget _buildFeatureVisual(_VipFeatureData item, {required bool isWide}) {
final url = item.resourceUrl?.trim();
if (url != null && url.isNotEmpty) {
return netImage(
url: url,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget:
item.assetPath != null
? Image.asset(item.assetPath!, fit: BoxFit.contain)
: isWide
? Center(child: _buildWideRewardPlaceholder())
: _buildRewardPlaceholder(item.icon ?? Icons.auto_awesome),
);
}
if (item.assetPath != null) {
return Image.asset(item.assetPath!, fit: BoxFit.contain);
}
return isWide
? Center(child: _buildWideRewardPlaceholder())
: _buildRewardPlaceholder(item.icon ?? Icons.auto_awesome);
}
Widget _buildRewardPlaceholder(IconData icon) { Widget _buildRewardPlaceholder(IconData icon) {
return Center( return Center(
child: Container( child: Container(
@ -749,7 +1060,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
required double width, required double width,
required double height, required double height,
}) { }) {
final isWide = item.title == 'Entry Effect'; final isWide = item.wide;
return Container( return Container(
width: width, width: width,
height: height, height: height,
@ -762,17 +1073,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (item.assetPath != null) Expanded(child: _buildFeatureVisual(item, isWide: isWide)),
Expanded(child: Image.asset(item.assetPath!, fit: BoxFit.contain))
else
Expanded(
child:
isWide
? Center(child: _buildWideRewardPlaceholder())
: _buildRewardPlaceholder(
item.icon ?? Icons.auto_awesome,
),
),
SizedBox(height: 5.w), SizedBox(height: 5.w),
Text( Text(
item.title, item.title,
@ -1107,11 +1408,19 @@ class _VipOrnamentLayout {
} }
class _VipFeatureData { class _VipFeatureData {
const _VipFeatureData(this.title, this.assetPath, [this.icon]); const _VipFeatureData(
this.title,
this.assetPath, {
this.icon,
this.resourceUrl,
this.wide = false,
});
final String title; final String title;
final String? assetPath; final String? assetPath;
final IconData? icon; final IconData? icon;
final String? resourceUrl;
final bool wide;
} }
class _VipActionButtonConfig { class _VipActionButtonConfig {

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart';
@ -13,6 +13,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart';
@ -75,6 +76,30 @@ class _RoomStartupAsyncResult<T> {
} }
} }
enum _PendingRoomStartupSeatActionType { clickSeat, goUpMic }
class _PendingRoomStartupSeatAction {
const _PendingRoomStartupSeatAction.clickSeat({
required this.index,
this.clickUser,
}) : type = _PendingRoomStartupSeatActionType.clickSeat,
eventType = null,
inviterId = null;
const _PendingRoomStartupSeatAction.goUpMic({
required this.index,
this.eventType,
this.inviterId,
}) : type = _PendingRoomStartupSeatActionType.goUpMic,
clickUser = null;
final _PendingRoomStartupSeatActionType type;
final num index;
final SocialChatUserProfile? clickUser;
final String? eventType;
final String? inviterId;
}
class RealTimeCommunicationManager extends ChangeNotifier { class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _micListPollingInterval = Duration(seconds: 2);
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
@ -86,6 +111,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
static const Duration _agoraJoinTimeout = Duration(seconds: 8); static const Duration _agoraJoinTimeout = Duration(seconds: 8);
static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12); static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12);
static const String _roomStartupSeatLoadingTag =
'roomStartupSeatInteractionLoading';
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
static const int _selfMicGoUpFailureLimit = 3;
bool needUpDataUserInfo = false; bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false; bool _roomVisualEffectsEnabled = false;
@ -113,6 +142,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool _disableMicListRefreshForCurrentSession = false; bool _disableMicListRefreshForCurrentSession = false;
bool _disableOnlineUsersRefreshForCurrentSession = false; bool _disableOnlineUsersRefreshForCurrentSession = false;
bool _isSelfMicActionInFlight = false; bool _isSelfMicActionInFlight = false;
bool _roomStartupSeatLoadingVisible = false;
bool _selfMicGoUpRetryLoadingVisible = false;
int _selfMicGoUpConsecutiveFailureCount = 0;
_PendingRoomStartupSeatAction? _pendingRoomStartupSeatAction;
bool _pendingMicListRefresh = false; bool _pendingMicListRefresh = false;
bool _pendingMicListRefreshNotifyIfUnchanged = false; bool _pendingMicListRefreshNotifyIfUnchanged = false;
Timer? _deferredMicListRefreshTimer; Timer? _deferredMicListRefreshTimer;
@ -157,6 +190,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
/// ///
bool isMic = true; bool isMic = true;
bool shouldShowSeatSoundWave(num index) {
final seat = roomWheatMap[index];
final userId = (seat?.user?.id ?? "").trim();
if (userId.isEmpty || (seat?.micMute ?? false)) {
return false;
}
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (userId == currentUserId && isMic) {
return false;
}
return true;
}
///线 ///线
List<SocialChatUserProfile> onlineUsers = []; List<SocialChatUserProfile> onlineUsers = [];
@ -191,6 +240,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get shouldShowRoomVisualEffects => bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled; currenRoom != null && _roomVisualEffectsEnabled;
bool get _shouldDeferSeatInteractionForRoomStartup =>
_roomStartupStatus == RoomStartupStatus.loading;
void setRoomVisualEffectsEnabled(bool enabled) { void setRoomVisualEffectsEnabled(bool enabled) {
if (_roomVisualEffectsEnabled == enabled) { if (_roomVisualEffectsEnabled == enabled) {
return; return;
@ -217,6 +269,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
_roomStartupStatus = RoomStartupStatus.ready; _roomStartupStatus = RoomStartupStatus.ready;
notifyListeners(); notifyListeners();
_finishRoomStartupSeatLoading(executePendingAction: true);
} }
void _setRoomStartupFailed(RoomStartupFailureType failureType) { void _setRoomStartupFailed(RoomStartupFailureType failureType) {
@ -225,6 +278,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomStartupFailureType = failureType; _roomStartupFailureType = failureType;
_roomStartupFailureToken += 1; _roomStartupFailureToken += 1;
_roomStartupStatus = RoomStartupStatus.failed; _roomStartupStatus = RoomStartupStatus.failed;
_finishRoomStartupSeatLoading(executePendingAction: false);
notifyListeners(); notifyListeners();
} }
@ -238,6 +292,125 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
} }
void _deferRoomStartupSeatAction(_PendingRoomStartupSeatAction action) {
_pendingRoomStartupSeatAction = action;
_showRoomStartupSeatLoading();
}
void _showRoomStartupSeatLoading() {
if (_roomStartupSeatLoadingVisible) {
return;
}
_roomStartupSeatLoadingVisible = true;
SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag);
SmartDialog.show(
tag: _roomStartupSeatLoadingTag,
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
backType: SmartBackType.block,
clickMaskDismiss: false,
maskColor: Colors.transparent,
builder: (_) {
return Center(
child: Container(
width: 55,
height: 55,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8),
),
child: const Center(child: SCRotatingDotsLoading(size: 28)),
),
);
},
);
}
void _showSelfMicGoUpRetryLoading() {
if (_selfMicGoUpRetryLoadingVisible) {
return;
}
_selfMicGoUpRetryLoadingVisible = true;
SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag);
SmartDialog.show(
tag: _selfMicGoUpRetryLoadingTag,
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
backType: SmartBackType.normal,
clickMaskDismiss: true,
maskColor: Colors.transparent,
onDismiss: () {
_selfMicGoUpRetryLoadingVisible = false;
},
builder: (_) {
return Center(
child: Container(
width: 55,
height: 55,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8),
),
child: const Center(child: SCRotatingDotsLoading(size: 28)),
),
);
},
);
}
void _dismissSelfMicGoUpRetryLoading() {
if (!_selfMicGoUpRetryLoadingVisible) {
return;
}
_selfMicGoUpRetryLoadingVisible = false;
SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag);
}
void _resetSelfMicGoUpFailureState() {
_selfMicGoUpConsecutiveFailureCount = 0;
_dismissSelfMicGoUpRetryLoading();
}
void _handleSelfMicGoUpFailure(String message) {
_selfMicGoUpConsecutiveFailureCount += 1;
if (_selfMicGoUpConsecutiveFailureCount >= _selfMicGoUpFailureLimit) {
SCTts.show(
'Failed to put on the microphone after multiple attempts, please try again later',
);
return;
}
SCTts.show(message);
}
void _finishRoomStartupSeatLoading({required bool executePendingAction}) {
final pendingAction = _pendingRoomStartupSeatAction;
_pendingRoomStartupSeatAction = null;
if (_roomStartupSeatLoadingVisible) {
_roomStartupSeatLoadingVisible = false;
SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag);
}
if (!executePendingAction || pendingAction == null) {
return;
}
Future<void>.microtask(() {
if (_roomStartupStatus != RoomStartupStatus.ready) {
return;
}
switch (pendingAction.type) {
case _PendingRoomStartupSeatActionType.clickSeat:
clickSite(pendingAction.index, clickUser: pendingAction.clickUser);
break;
case _PendingRoomStartupSeatActionType.goUpMic:
shangMai(
pendingAction.index,
eventType: pendingAction.eventType,
inviterId: pendingAction.inviterId,
);
break;
}
});
}
void _startRoomStatePolling() { void _startRoomStatePolling() {
_stopRoomStatePolling(); _stopRoomStatePolling();
if ((currenRoom?.roomProfile?.roomProfile?.id ?? "").isEmpty) { if ((currenRoom?.roomProfile?.roomProfile?.id ?? "").isEmpty) {
@ -1188,6 +1361,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future<void> setSelfMicMutedFromBottom(bool muted) async { Future<void> setSelfMicMutedFromBottom(bool muted) async {
isMic = muted; isMic = muted;
notifyListeners();
final currentUserId = final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
final seatIndex = userOnMaiInIndex(currentUserId); final seatIndex = userOnMaiInIndex(currentUserId);
@ -1523,6 +1697,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (seatIndex == null) { if (seatIndex == null) {
continue; continue;
} }
if (!shouldShowSeatSoundWave(seatIndex)) {
continue;
}
roomWheatMap[seatIndex]?.setVolume = volume; roomWheatMap[seatIndex]?.setVolume = volume;
for (final listener in listeners) { for (final listener in listeners) {
@ -1640,6 +1817,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return; return;
} }
rtmProvider = Provider.of<RtmProvider>(context, listen: false); rtmProvider = Provider.of<RtmProvider>(context, listen: false);
_clearRoomContributionLevelData(context);
rtmProvider?.seedSystemRoomTips( rtmProvider?.seedSystemRoomTips(
SCAppLocalizations.of(context)!.systemRoomTips, SCAppLocalizations.of(context)!.systemRoomTips,
); );
@ -2205,6 +2383,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void _clearData() { void _clearData() {
_roomEntryRequestSerial += 1; _roomEntryRequestSerial += 1;
_previewRoomSeatCount = null; _previewRoomSeatCount = null;
_finishRoomStartupSeatLoading(executePendingAction: false);
_resetSelfMicGoUpFailureState();
_clearRoomContributionLevelData();
_stopRoomStatePolling(); _stopRoomStatePolling();
_resetHeartbeatTracking(); _resetHeartbeatTracking();
_resetAgoraTracking(); _resetAgoraTracking();
@ -2245,6 +2426,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
} }
void _clearRoomContributionLevelData([BuildContext? targetContext]) {
final currentContext = targetContext ?? context;
if (currentContext == null || !currentContext.mounted) {
return;
}
Provider.of<SocialChatRoomManager>(
currentContext,
listen: false,
).clearContributionLevelData();
}
@override @override
void dispose() { void dispose() {
_stopRoomStatePolling(); _stopRoomStatePolling();
@ -2260,6 +2452,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
/// ///
void clickSite(num index, {SocialChatUserProfile? clickUser}) { void clickSite(num index, {SocialChatUserProfile? clickUser}) {
if (_shouldDeferSeatInteractionForRoomStartup) {
_deferRoomStartupSeatAction(
_PendingRoomStartupSeatAction.clickSeat(
index: index,
clickUser: clickUser,
),
);
return;
}
if (_handleDirectSeatInteraction(index, clickUser: clickUser)) { if (_handleDirectSeatInteraction(index, clickUser: clickUser)) {
return; return;
} }
@ -2396,6 +2598,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
void shangMai(num index, {String? eventType, String? inviterId}) async { void shangMai(num index, {String? eventType, String? inviterId}) async {
if (_shouldDeferSeatInteractionForRoomStartup) {
_deferRoomStartupSeatAction(
_PendingRoomStartupSeatAction.goUpMic(
index: index,
eventType: eventType,
inviterId: inviterId,
),
);
return;
}
var myUser = AccountStorage().getCurrentUser()?.userProfile; var myUser = AccountStorage().getCurrentUser()?.userProfile;
if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) {
} else { } else {
@ -2407,9 +2620,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
} }
final shouldShowRetryLoading = _selfMicGoUpConsecutiveFailureCount > 0;
if (_isSelfMicActionInFlight) { if (_isSelfMicActionInFlight) {
if (shouldShowRetryLoading) {
_showSelfMicGoUpRetryLoading();
}
return; return;
} }
if (shouldShowRetryLoading) {
_showSelfMicGoUpRetryLoading();
}
_isSelfMicActionInFlight = true; _isSelfMicActionInFlight = true;
try { try {
var micGoUpRes = await SCChatRoomRepository().micGoUp( var micGoUpRes = await SCChatRoomRepository().micGoUp(
@ -2441,7 +2661,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_syncSelfMicRuntimeState(); _syncSelfMicRuntimeState();
notifyListeners(); notifyListeners();
_refreshMicListSilently(notifyIfUnchanged: true); _refreshMicListSilently(notifyIfUnchanged: true);
SCTts.show('Failed to put on the microphone, Agora switch failed'); _handleSelfMicGoUpFailure(
'Failed to put on the microphone, Agora switch failed',
);
return; return;
} }
@ -2489,13 +2711,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
_refreshMicListSilently(notifyIfUnchanged: true); _refreshMicListSilently(notifyIfUnchanged: true);
_resetSelfMicGoUpFailureState();
} catch (ex) { } catch (ex) {
if (_isCancelledRequest(ex)) { if (_isCancelledRequest(ex)) {
_refreshMicListSilently(notifyIfUnchanged: true); _refreshMicListSilently(notifyIfUnchanged: true);
return; return;
} }
SCTts.show('Failed to put on the microphone, ${_errorMessageFrom(ex)}'); _handleSelfMicGoUpFailure(
'Failed to put on the microphone, ${_errorMessageFrom(ex)}',
);
} finally { } finally {
_dismissSelfMicGoUpRetryLoading();
_isSelfMicActionInFlight = false; _isSelfMicActionInFlight = false;
} }
} }

View File

@ -9,11 +9,12 @@ import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; import 'package:yumi/shared/business_logic/models/res/my_room_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart';
class SocialChatRoomManager extends ChangeNotifier { class SocialChatRoomManager extends ChangeNotifier {
MyRoomRes? myRoom; MyRoomRes? myRoom;
SCRoomContributeLevelRes? roomContributeLevelRes; SCRoomContributeLevelRes? roomContributeLevelRes;
bool isMyRoomLoading = false; String? _roomContributeLevelRoomId;
bool hasLoadedMyRoom = false; bool isMyRoomLoading = false;
bool hasLoadedMyRoom = false;
Future<void> fetchMyRoomData() async { Future<void> fetchMyRoomData() async {
if (isMyRoomLoading) { if (isMyRoomLoading) {
@ -73,13 +74,41 @@ class SocialChatRoomManager extends ChangeNotifier {
SCLoadingManager.hide(); SCLoadingManager.hide();
} }
void fetchContributionLevelData(String roomId, {bool forceRefresh = false}) { void clearContributionLevelData({bool notify = true}) {
if (forceRefresh) { _roomContributeLevelRoomId = null;
roomContributeLevelRes = null; if (roomContributeLevelRes == null) {
} return;
SCChatRoomRepository().roomContributionActivity(roomId).then((value) { }
roomContributeLevelRes = value; roomContributeLevelRes = null;
notifyListeners(); if (notify) {
}); notifyListeners();
} }
} }
void fetchContributionLevelData(String roomId, {bool forceRefresh = false}) {
final resolvedRoomId = roomId.trim();
if (resolvedRoomId.isEmpty) {
clearContributionLevelData();
return;
}
final shouldClear =
forceRefresh || _roomContributeLevelRoomId != resolvedRoomId;
_roomContributeLevelRoomId = resolvedRoomId;
if (shouldClear && roomContributeLevelRes != null) {
roomContributeLevelRes = null;
notifyListeners();
}
SCChatRoomRepository()
.roomContributionActivity(resolvedRoomId)
.then((value) {
if (_roomContributeLevelRoomId != resolvedRoomId) {
return;
}
roomContributeLevelRes = value;
notifyListeners();
})
.catchError((_) {});
}
}

View File

@ -1,8 +1,20 @@
class SCVipResourceRes { class SCVipResourceRes {
SCVipResourceRes({String? resourceId, String? name, String? url}) { SCVipResourceRes({
String? resourceId,
String? name,
String? url,
String? sourceUrl,
String? resourceUrl,
String? cover,
String? coverUrl,
}) {
_resourceId = resourceId; _resourceId = resourceId;
_name = name; _name = name;
_url = url; _url = url;
_sourceUrl = sourceUrl;
_resourceUrl = resourceUrl;
_cover = cover;
_coverUrl = coverUrl;
} }
SCVipResourceRes.fromJson(dynamic json) { SCVipResourceRes.fromJson(dynamic json) {
@ -11,11 +23,19 @@ class SCVipResourceRes {
_resourceId = _stringValue(map['resourceId']); _resourceId = _stringValue(map['resourceId']);
_name = _stringValue(map['name']); _name = _stringValue(map['name']);
_url = _stringValue(map['url']); _url = _stringValue(map['url']);
_sourceUrl = _stringValue(map['sourceUrl']);
_resourceUrl = _stringValue(map['resourceUrl']);
_cover = _stringValue(map['cover']);
_coverUrl = _stringValue(map['coverUrl']);
} }
String? _resourceId; String? _resourceId;
String? _name; String? _name;
String? _url; String? _url;
String? _sourceUrl;
String? _resourceUrl;
String? _cover;
String? _coverUrl;
String? get resourceId => _resourceId; String? get resourceId => _resourceId;
@ -23,11 +43,57 @@ class SCVipResourceRes {
String? get url => _url; String? get url => _url;
String? get sourceUrl => _sourceUrl;
String? get resourceUrl => _resourceUrl;
String? get cover => _cover;
String? get coverUrl => _coverUrl;
String? get previewUrl {
final cover = _firstNonBlank([_coverUrl, _cover]);
if (cover != null) return cover;
final staticUrl = _firstNonBlank(
[_url, _resourceUrl, _sourceUrl].where((value) {
final text = value?.trim();
return text != null && text.isNotEmpty && !_isSvgaUrl(text);
}),
);
if (staticUrl != null) return staticUrl;
return _firstNonBlank([_url, _resourceUrl, _sourceUrl]);
}
String? get sourceResourceUrl =>
_firstNonBlank([_sourceUrl, _resourceUrl, _url]);
bool get hasIdentity {
final id = _resourceId?.trim();
return id != null && id.isNotEmpty && id != '0';
}
bool get hasAnyUrl =>
_firstNonBlank([_url, _sourceUrl, _resourceUrl, _cover, _coverUrl]) !=
null;
bool get hasPreviewUrl => previewUrl != null;
bool get hasSourceUrl => sourceResourceUrl != null;
bool get hasContent =>
hasAnyUrl || hasIdentity || (_name?.trim().isNotEmpty ?? false);
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['resourceId'] = _resourceId; map['resourceId'] = _resourceId;
map['name'] = _name; map['name'] = _name;
map['url'] = _url; map['url'] = _url;
map['sourceUrl'] = _sourceUrl;
map['resourceUrl'] = _resourceUrl;
map['cover'] = _cover;
map['coverUrl'] = _coverUrl;
return map; return map;
} }
} }
@ -660,3 +726,18 @@ bool? _boolValue(dynamic value) {
if (lower == 'false') return false; if (lower == 'false') return false;
return null; return null;
} }
String? _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return null;
}
bool _isSvgaUrl(String value) {
final lower = value.toLowerCase();
return lower.endsWith('.svga') || lower.contains('.svga?');
}

View File

@ -213,11 +213,11 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
'level': result.level, 'level': result.level,
'levelCode': result.levelCode, 'levelCode': result.levelCode,
'expireAt': result.expireAt, 'expireAt': result.expireAt,
'hasBadge': result.badge?.url?.isNotEmpty == true, 'badge': _resourceSummary(result.badge),
'hasAvatarFrame': result.avatarFrame?.url?.isNotEmpty == true, 'avatarFrame': _resourceSummary(result.avatarFrame),
'hasEntryEffect': result.entryEffect?.url?.isNotEmpty == true, 'entryEffect': _resourceSummary(result.entryEffect),
'hasChatBubble': result.chatBubble?.url?.isNotEmpty == true, 'chatBubble': _resourceSummary(result.chatBubble),
'hasFloatPicture': result.floatPicture?.url?.isNotEmpty == true, 'floatPicture': _resourceSummary(result.floatPicture),
}; };
} }
@ -237,13 +237,31 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
'priceGold': level.priceGold, 'priceGold': level.priceGold,
'canPurchase': level.canPurchase, 'canPurchase': level.canPurchase,
'payableGold': level.payableGold, 'payableGold': level.payableGold,
'hasBadge': level.badge?.url?.isNotEmpty == true, 'badge': _resourceSummary(level.badge),
'avatarFrame': _resourceSummary(level.avatarFrame),
'entryEffect': _resourceSummary(level.entryEffect),
'chatBubble': _resourceSummary(level.chatBubble),
'floatPicture': _resourceSummary(level.floatPicture),
}, },
) )
.toList(), .toList(),
}; };
} }
static Map<String, dynamic>? _resourceSummary(SCVipResourceRes? resource) {
if (resource?.hasContent != true) {
return null;
}
return {
'id': resource?.resourceId,
'name': resource?.name,
'hasPreviewUrl': resource?.hasPreviewUrl == true,
'hasSourceUrl': resource?.hasSourceUrl == true,
'previewUrl': resource?.previewUrl,
'sourceUrl': resource?.sourceResourceUrl,
};
}
static Map<String, dynamic> _previewSummary(SCVipPreviewRes result) { static Map<String, dynamic> _previewSummary(SCVipPreviewRes result) {
return { return {
'orderType': result.orderType, 'orderType': result.orderType,