vip接口 以及房间兜底
This commit is contained in:
parent
9f4df82c42
commit
fd445ccdaa
File diff suppressed because it is too large
Load Diff
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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((_) {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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?');
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user