修复
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'http://192.168.110.66:1100/',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.66:1100/”,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
|
||||
75
lib/debug/first_recharge_dialog_preview.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:yumi/ui_kit/widgets/first_recharge/first_recharge_dialog.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
runApp(const FirstRechargeDialogPreviewApp());
|
||||
}
|
||||
|
||||
class FirstRechargeDialogPreviewApp extends StatelessWidget {
|
||||
const FirstRechargeDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _FirstRechargeDialogPreviewPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FirstRechargeDialogPreviewPage extends StatefulWidget {
|
||||
const _FirstRechargeDialogPreviewPage();
|
||||
|
||||
@override
|
||||
State<_FirstRechargeDialogPreviewPage> createState() =>
|
||||
_FirstRechargeDialogPreviewPageState();
|
||||
}
|
||||
|
||||
class _FirstRechargeDialogPreviewPageState
|
||||
extends State<_FirstRechargeDialogPreviewPage> {
|
||||
bool _shown = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_shown) {
|
||||
return;
|
||||
}
|
||||
_shown = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
FirstRechargeDialog.show(
|
||||
context,
|
||||
data: FirstRechargeDialogData.mock(),
|
||||
onBuyNow: (_) {},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Image.asset(
|
||||
'sc_images/first_recharge/preview_background.png',
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
173
lib/debug/gift_page_preview.dart
Normal file
@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/config/app_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/modules/gift/gift_page.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
AppConfig.initialize();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await DataPersistence.initialize();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
runApp(const GiftPagePreviewApp());
|
||||
}
|
||||
|
||||
class GiftPagePreviewApp extends StatelessWidget {
|
||||
const GiftPagePreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<SCAppGeneralManager>(
|
||||
create: (_) => _PreviewAppGeneralManager(),
|
||||
),
|
||||
ChangeNotifierProvider<RtcProvider>(create: (_) => RtcProvider()),
|
||||
ChangeNotifierProvider<RtmProvider>(create: (_) => RtmProvider()),
|
||||
ChangeNotifierProvider<SocialChatUserProfileManager>(
|
||||
create: (_) => _PreviewUserProfileManager(),
|
||||
),
|
||||
],
|
||||
child: ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: SCAppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
SCAppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _GiftPagePreviewHome(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GiftPagePreviewHome extends StatefulWidget {
|
||||
const _GiftPagePreviewHome();
|
||||
|
||||
@override
|
||||
State<_GiftPagePreviewHome> createState() => _GiftPagePreviewHomeState();
|
||||
}
|
||||
|
||||
class _GiftPagePreviewHomeState extends State<_GiftPagePreviewHome> {
|
||||
bool _shown = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_shown) {
|
||||
return;
|
||||
}
|
||||
_shown = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.show(
|
||||
tag: "showGiftControl",
|
||||
alignment: Alignment.bottomCenter,
|
||||
maskColor: Colors.transparent,
|
||||
animationType: SmartAnimationType.fade,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => const GiftPage(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/room/sc_icon_room_defaut_bg.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => const ColoredBox(color: Color(0xFF062320)),
|
||||
),
|
||||
const Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 88),
|
||||
child: Text(
|
||||
'Gift panel preview',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewAppGeneralManager extends SCAppGeneralManager {
|
||||
_PreviewAppGeneralManager() {
|
||||
final gifts = List<SocialChatGiftRes>.generate(
|
||||
8,
|
||||
(index) => SocialChatGiftRes(
|
||||
id: 'preview_$index',
|
||||
giftName: 'Gift ${index + 1}',
|
||||
giftCandy: (index + 1) * 10,
|
||||
giftTab: 'ALL',
|
||||
giftPhoto: '',
|
||||
),
|
||||
);
|
||||
giftResList = gifts;
|
||||
giftByTab = {'ALL': gifts};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fetchCountryList() async {}
|
||||
|
||||
@override
|
||||
Future<void> giftList({
|
||||
bool includeCustomized = true,
|
||||
bool forceRefresh = false,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
void giftActivityList() {}
|
||||
|
||||
@override
|
||||
Future<void> giftBackpack({bool forceRefresh = false}) async {}
|
||||
|
||||
@override
|
||||
Future<void> warmGiftPageCovers(
|
||||
String type, {
|
||||
required int pageIndex,
|
||||
int pageCount = 1,
|
||||
int pageSize = 8,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
bool isGiftTabLoading(String type) => false;
|
||||
}
|
||||
|
||||
class _PreviewUserProfileManager extends SocialChatUserProfileManager {
|
||||
@override
|
||||
void balance() {}
|
||||
}
|
||||
164
lib/debug/room_animation_switch_dialog_preview.dart
Normal file
@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_animation_switch_dialog.dart';
|
||||
|
||||
void main() {
|
||||
SCGlobalConfig.isGiftSpecialEffects = true;
|
||||
SCGlobalConfig.isLuckGiftSpecialEffects = true;
|
||||
SCGlobalConfig.isEntryVehicleAnimation = false;
|
||||
runApp(const RoomAnimationSwitchDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomAnimationSwitchDialogPreviewApp extends StatelessWidget {
|
||||
const RoomAnimationSwitchDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: SCAppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
SCAppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _RoomAnimationSwitchDialogPreviewPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomAnimationSwitchDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomAnimationSwitchDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: const [
|
||||
_PreviewRoomBackground(),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: RoomAnimationSwitchDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewRoomBackground extends StatelessWidget {
|
||||
const _PreviewRoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF172F35), Color(0xFF071B17), Color(0xFF1B1308)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 24.w,
|
||||
top: 70.w,
|
||||
child: _PreviewLine(width: 128.w, opacity: 0.55),
|
||||
),
|
||||
for (var index = 0; index < 10; index++)
|
||||
Positioned(
|
||||
left: (38 + (index % 5) * 66).w,
|
||||
top: (145 + (index ~/ 5) * 80).w,
|
||||
child: const _PreviewSeat(),
|
||||
),
|
||||
Positioned(
|
||||
left: 26.w,
|
||||
right: 26.w,
|
||||
top: 355.w,
|
||||
child: _PreviewPanel(height: 150.w),
|
||||
),
|
||||
Positioned(
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
bottom: 36.w,
|
||||
child: _PreviewPanel(height: 52.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSeat extends StatelessWidget {
|
||||
const _PreviewSeat();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFCE7C).withValues(alpha: 0.36),
|
||||
width: 2.w,
|
||||
),
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
),
|
||||
child: SizedBox(width: 44.w, height: 44.w),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewLine extends StatelessWidget {
|
||||
const _PreviewLine({required this.width, required this.opacity});
|
||||
|
||||
final double width;
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: 14.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: opacity),
|
||||
borderRadius: BorderRadius.circular(7.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewPanel extends StatelessWidget {
|
||||
const _PreviewPanel({required this.height});
|
||||
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.24),
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/debug/room_recharge_bottom_sheet_preview.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/payment/google_payment_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_recharge_bottom_sheet.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
runApp(const RoomRechargeBottomSheetPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRechargeBottomSheetPreviewApp extends StatelessWidget {
|
||||
const RoomRechargeBottomSheetPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<SocialChatUserProfileManager>(
|
||||
create: (_) => _PreviewUserProfileManager(),
|
||||
),
|
||||
ChangeNotifierProvider<AndroidPaymentProcessor>(
|
||||
create: (_) => AndroidPaymentProcessor(),
|
||||
),
|
||||
],
|
||||
child: ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: SCAppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
SCAppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _RoomRechargeBottomSheetPreviewPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRechargeBottomSheetPreviewPage extends StatefulWidget {
|
||||
const _RoomRechargeBottomSheetPreviewPage();
|
||||
|
||||
@override
|
||||
State<_RoomRechargeBottomSheetPreviewPage> createState() =>
|
||||
_RoomRechargeBottomSheetPreviewPageState();
|
||||
}
|
||||
|
||||
class _RoomRechargeBottomSheetPreviewPageState
|
||||
extends State<_RoomRechargeBottomSheetPreviewPage> {
|
||||
bool _shown = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_shown) {
|
||||
return;
|
||||
}
|
||||
_shown = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
RoomRechargeBottomSheet.show(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Image.asset(
|
||||
'sc_images/room/sc_icon_room_defaut_bg.png',
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewUserProfileManager extends SocialChatUserProfileManager {
|
||||
@override
|
||||
void balance() {
|
||||
updateBalance(0);
|
||||
}
|
||||
}
|
||||
@ -276,6 +276,8 @@ class RootAppWithProviders extends StatelessWidget {
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
final RouteObserver<ModalRoute<dynamic>> modalRouteObserver =
|
||||
RouteObserver<ModalRoute<dynamic>>();
|
||||
|
||||
class YumiApplication extends StatefulWidget {
|
||||
const YumiApplication({super.key});
|
||||
@ -429,7 +431,7 @@ class _YumiApplicationState extends State<YumiApplication>
|
||||
);
|
||||
},
|
||||
),
|
||||
navigatorObservers: [routeObserver],
|
||||
navigatorObservers: [routeObserver, modalRouteObserver],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1663,6 +1663,26 @@ class _MessageItem extends StatelessWidget {
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: inviterName,
|
||||
avatarUrl: cpInvite?.userAvatar ?? "",
|
||||
headdress:
|
||||
isInviter
|
||||
? currentProfile?.getHeaddress()?.sourceUrl ?? ""
|
||||
: _firstNonBlankString([
|
||||
cpInvite?.userHeaddress,
|
||||
data["userHeaddress"]?.toString(),
|
||||
data["userAvatarFrame"]?.toString(),
|
||||
content["userHeaddress"]?.toString(),
|
||||
content["userAvatarFrame"]?.toString(),
|
||||
], fallback: ""),
|
||||
headdressCover:
|
||||
isInviter
|
||||
? currentProfile?.getHeaddress()?.cover ?? ""
|
||||
: _firstNonBlankString([
|
||||
cpInvite?.userHeaddressCover,
|
||||
data["userHeaddressCover"]?.toString(),
|
||||
data["userAvatarFrameCover"]?.toString(),
|
||||
content["userHeaddressCover"]?.toString(),
|
||||
content["userAvatarFrameCover"]?.toString(),
|
||||
], fallback: ""),
|
||||
),
|
||||
receiver: RoomCpInviteDialogUser(
|
||||
name: receiverName,
|
||||
@ -1673,6 +1693,24 @@ class _MessageItem extends StatelessWidget {
|
||||
content["acceptUserAvatar"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.userAvatar ?? "",
|
||||
headdress:
|
||||
isInviter
|
||||
? _firstNonBlankString([
|
||||
data["acceptHeaddress"]?.toString(),
|
||||
data["acceptAvatarFrame"]?.toString(),
|
||||
content["acceptHeaddress"]?.toString(),
|
||||
content["acceptAvatarFrame"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover:
|
||||
isInviter
|
||||
? _firstNonBlankString([
|
||||
data["acceptHeaddressCover"]?.toString(),
|
||||
data["acceptAvatarFrameCover"]?.toString(),
|
||||
content["acceptHeaddressCover"]?.toString(),
|
||||
content["acceptAvatarFrameCover"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
style:
|
||||
isInviter
|
||||
|
||||
@ -759,6 +759,26 @@ class _MessageItem extends StatelessWidget {
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: inviterName,
|
||||
avatarUrl: cpInvite?.userAvatar ?? "",
|
||||
headdress:
|
||||
isInviter
|
||||
? currentProfile?.getHeaddress()?.sourceUrl ?? ""
|
||||
: _firstNonBlankString([
|
||||
cpInvite?.userHeaddress,
|
||||
data["userHeaddress"]?.toString(),
|
||||
data["userAvatarFrame"]?.toString(),
|
||||
content["userHeaddress"]?.toString(),
|
||||
content["userAvatarFrame"]?.toString(),
|
||||
], fallback: ""),
|
||||
headdressCover:
|
||||
isInviter
|
||||
? currentProfile?.getHeaddress()?.cover ?? ""
|
||||
: _firstNonBlankString([
|
||||
cpInvite?.userHeaddressCover,
|
||||
data["userHeaddressCover"]?.toString(),
|
||||
data["userAvatarFrameCover"]?.toString(),
|
||||
content["userHeaddressCover"]?.toString(),
|
||||
content["userAvatarFrameCover"]?.toString(),
|
||||
], fallback: ""),
|
||||
),
|
||||
receiver: RoomCpInviteDialogUser(
|
||||
name: receiverName,
|
||||
@ -769,6 +789,24 @@ class _MessageItem extends StatelessWidget {
|
||||
content["acceptUserAvatar"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.userAvatar ?? "",
|
||||
headdress:
|
||||
isInviter
|
||||
? _firstNonBlankString([
|
||||
data["acceptHeaddress"]?.toString(),
|
||||
data["acceptAvatarFrame"]?.toString(),
|
||||
content["acceptHeaddress"]?.toString(),
|
||||
content["acceptAvatarFrame"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover:
|
||||
isInviter
|
||||
? _firstNonBlankString([
|
||||
data["acceptHeaddressCover"]?.toString(),
|
||||
data["acceptAvatarFrameCover"]?.toString(),
|
||||
content["acceptHeaddressCover"]?.toString(),
|
||||
content["acceptAvatarFrameCover"]?.toString(),
|
||||
], fallback: "")
|
||||
: currentProfile?.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
style:
|
||||
isInviter
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
|
||||
enum _CpGuideTab { note, rights }
|
||||
|
||||
@ -40,6 +41,7 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
final Map<_CpRightsCategory, List<_CpRightsLevel>> _rightsRowsByCategory = {};
|
||||
final Set<_CpRightsCategory> _loadingRightsCategories = {};
|
||||
final Set<_CpRightsCategory> _loadedRightsCategories = {};
|
||||
final Set<_CpRightsCategory> _failedRightsCategories = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -388,6 +390,9 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
final isInitialLoading =
|
||||
_loadingRightsCategories.contains(_rightsCategory) &&
|
||||
!_rightsRowsByCategory.containsKey(_rightsCategory);
|
||||
final hasLoadError =
|
||||
_failedRightsCategories.contains(_rightsCategory) &&
|
||||
!_rightsRowsByCategory.containsKey(_rightsCategory);
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -402,21 +407,24 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 13.w),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_buildRightsTable(rows),
|
||||
if (isInitialLoading)
|
||||
SizedBox(
|
||||
width: 22.w,
|
||||
height: 22.w,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.w,
|
||||
color: _accentColor,
|
||||
if (hasLoadError)
|
||||
_buildRightsLoadError(_rightsCategory)
|
||||
else
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_buildRightsTable(rows),
|
||||
if (isInitialLoading)
|
||||
SizedBox(
|
||||
width: 22.w,
|
||||
height: 22.w,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.w,
|
||||
color: _accentColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -461,23 +469,54 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRightsLoadError(_CpRightsCategory category) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _ensureRightsConfigLoaded(category, force: true),
|
||||
child: Container(
|
||||
width: 351.w,
|
||||
height: 156.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: Text(
|
||||
"Failed to load rights. Tap to retry.",
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: _bodyStyle(color: _accentColor, lineHeight: 1.3),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<_CpRightsLevel> _rightsRowsFor(_CpRightsCategory category) {
|
||||
switch (category) {
|
||||
case _CpRightsCategory.cp:
|
||||
return const [
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "0", rightSlots: 2),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "1,000,000", rightSlots: 3),
|
||||
_CpRightsLevel(level: "Lv.3", intimacy: "2,000,000", rightSlots: 3),
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
||||
];
|
||||
case _CpRightsCategory.brother:
|
||||
return const [
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "0", rightSlots: 2),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "800,000", rightSlots: 2),
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
||||
];
|
||||
case _CpRightsCategory.sisters:
|
||||
return const [
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "0", rightSlots: 2),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "800,000", rightSlots: 2),
|
||||
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
||||
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -496,7 +535,10 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
return;
|
||||
}
|
||||
final relationType = _relationTypeForRightsCategory(category);
|
||||
setState(() => _loadingRightsCategories.add(category));
|
||||
setState(() {
|
||||
_loadingRightsCategories.add(category);
|
||||
_failedRightsCategories.remove(category);
|
||||
});
|
||||
_rightsDebugLog(
|
||||
'load start relationType=$relationType cpUserId=${widget.cpUserId}',
|
||||
);
|
||||
@ -506,12 +548,16 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
cpUserId: widget.cpUserId,
|
||||
);
|
||||
final rows = _rightsRowsFromConfig(config, category);
|
||||
if (rows.isEmpty) {
|
||||
throw StateError('empty CP rights levels');
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_rightsRowsByCategory[category] = rows;
|
||||
_loadedRightsCategories.add(category);
|
||||
_failedRightsCategories.remove(category);
|
||||
_loadingRightsCategories.remove(category);
|
||||
});
|
||||
_rightsDebugLog(
|
||||
@ -521,7 +567,10 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _loadingRightsCategories.remove(category));
|
||||
setState(() {
|
||||
_loadingRightsCategories.remove(category);
|
||||
_failedRightsCategories.add(category);
|
||||
});
|
||||
_rightsDebugLog('load failed relationType=$relationType error=$error');
|
||||
}
|
||||
}
|
||||
@ -538,17 +587,17 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
rows.add(
|
||||
_CpRightsLevel(
|
||||
level:
|
||||
levelNumber != null
|
||||
? "Lv.$levelNumber"
|
||||
: (levelName?.isNotEmpty == true
|
||||
? levelName!
|
||||
levelName?.isNotEmpty == true
|
||||
? levelName!
|
||||
: (levelNumber != null
|
||||
? "Lv.$levelNumber"
|
||||
: "Lv.${rows.length + 1}"),
|
||||
intimacy: _formatRightsIntimacyValue(requiredValue),
|
||||
rightSlots: level.rights.isEmpty ? 1 : level.rights.length,
|
||||
rights: level.rights.map(_CpRightItem.fromRes).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return rows.isEmpty ? _rightsRowsFor(category) : rows;
|
||||
return rows;
|
||||
}
|
||||
|
||||
String _relationTypeForRightsCategory(_CpRightsCategory category) {
|
||||
@ -679,7 +728,11 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
for (var i = 0; i < row.rightSlots; i++) ...[
|
||||
SizedBox(
|
||||
height: rightCellHeight.w,
|
||||
child: Center(child: _buildRightPlaceholder()),
|
||||
child: Center(
|
||||
child: _buildRightCell(
|
||||
i < row.rights.length ? row.rights[i] : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (i != row.rightSlots - 1) _tableLine(width: 117),
|
||||
],
|
||||
@ -715,7 +768,47 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
||||
return Container(width: width.w, height: 1.w, color: Colors.white);
|
||||
}
|
||||
|
||||
Widget _buildRightPlaceholder() {
|
||||
Widget _buildRightCell(_CpRightItem? right) {
|
||||
final opacity = (right?.unlocked ?? false) ? 1.0 : 0.55;
|
||||
final title = right?.title.trim() ?? "";
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildRightIcon(right?.icon ?? ""),
|
||||
if (title.isNotEmpty) ...[
|
||||
SizedBox(height: 4.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: _bodyStyle(fontSize: 10, lineHeight: 1.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRightIcon(String icon) {
|
||||
if (icon.trim().isNotEmpty) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(9.w),
|
||||
child: netImage(
|
||||
url: icon,
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
fit: BoxFit.cover,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
@ -915,10 +1008,32 @@ class _CpRightsLevel {
|
||||
const _CpRightsLevel({
|
||||
required this.level,
|
||||
required this.intimacy,
|
||||
required this.rightSlots,
|
||||
this.rights = const [],
|
||||
});
|
||||
|
||||
final String level;
|
||||
final String intimacy;
|
||||
final int rightSlots;
|
||||
final List<_CpRightItem> rights;
|
||||
|
||||
int get rightSlots => rights.isEmpty ? 1 : rights.length;
|
||||
}
|
||||
|
||||
class _CpRightItem {
|
||||
const _CpRightItem({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.unlocked,
|
||||
});
|
||||
|
||||
factory _CpRightItem.fromRes(SCCpRightRes right) {
|
||||
return _CpRightItem(
|
||||
icon: right.icon?.trim() ?? "",
|
||||
title: right.title?.trim() ?? "",
|
||||
unlocked: right.unlocked ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
final String icon;
|
||||
final String title;
|
||||
final bool unlocked;
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_system_invit_message_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
@ -35,13 +36,13 @@ import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
|
||||
import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/modules/gift/gift_tab_page.dart';
|
||||
import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart';
|
||||
import 'package:yumi/services/gift/room_gift_combo_send_controller.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_recharge_bottom_sheet.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
import '../../shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart';
|
||||
@ -65,6 +66,8 @@ class GiftPage extends StatefulWidget {
|
||||
|
||||
class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
static const String _cpProfileAssetBase = "sc_images/room/cp_profile";
|
||||
static const String _firstRechargeGiftEntryCard =
|
||||
"sc_images/first_recharge/room_gift_entry_card.png";
|
||||
static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab;
|
||||
static const Duration _comboSendBatchWindow = Duration(milliseconds: 200);
|
||||
|
||||
@ -280,10 +283,12 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return _isCpGiftTab(_tabTypes[controller.index]);
|
||||
}
|
||||
|
||||
bool _isCpGiftTab(String tabType) =>
|
||||
tabType.trim().toUpperCase() == SCGiftType.CP.name;
|
||||
bool _isCpGiftTab(String tabType) => scIsCpGiftTab(tabType);
|
||||
|
||||
bool _isCpGift(SocialChatGiftRes? gift) => _isCpGiftTab(gift?.giftTab ?? "");
|
||||
bool _isCpGift(SocialChatGiftRes? gift) => scIsCpGift(gift);
|
||||
|
||||
String _cpRelationTypeForGift(SocialChatGiftRes gift) =>
|
||||
scCpRelationTypeForGift(gift);
|
||||
|
||||
bool get _isCpRecipientSingleMode =>
|
||||
_isCurrentCpGiftTab || _isCpGift(checkedGift);
|
||||
@ -640,6 +645,19 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return errorText.contains('INSUFFICIENT_BALANCE');
|
||||
}
|
||||
|
||||
bool _isInsufficientMoneyError(Object error, String resolvedMessage) {
|
||||
final errorText =
|
||||
'${_readBackendErrorText(error)} $resolvedMessage'.toLowerCase();
|
||||
return errorText.contains('no enough money') ||
|
||||
errorText.contains('not enough money') ||
|
||||
errorText.contains('insufficient money') ||
|
||||
errorText.contains('insufficient balance') ||
|
||||
errorText.contains('insufficient_balance') ||
|
||||
errorText.contains('balance not enough') ||
|
||||
errorText.contains('not enough gold') ||
|
||||
errorText.contains('insufficient gold');
|
||||
}
|
||||
|
||||
String _readBackendErrorText(Object error) {
|
||||
final values = <String>[];
|
||||
void addValue(dynamic value) {
|
||||
@ -684,6 +702,11 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
final currentTabType = giftTabs[tabController.index].type;
|
||||
final isCpTab = currentTabType == "CP";
|
||||
final showFirstRechargeEntry =
|
||||
!SCGlobalConfig.isReview &&
|
||||
!isCpTab &&
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.firstRecharge ??
|
||||
false);
|
||||
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
||||
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
||||
return SafeArea(
|
||||
@ -691,83 +714,14 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SCGlobalConfig.isReview
|
||||
? Container()
|
||||
: ((AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRecharge ??
|
||||
false)
|
||||
? GestureDetector(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
Transform.flip(
|
||||
flipX:
|
||||
SCGlobalConfig.lang == "ar"
|
||||
? true
|
||||
: false, // 水平翻转
|
||||
flipY: false, // 垂直翻转设为 false
|
||||
child: Image.asset(
|
||||
_strategy
|
||||
.getGiftPageFirstRechargeRoomTagIcon(),
|
||||
height: 75.w,
|
||||
),
|
||||
),
|
||||
SCGlobalConfig.lang == "ar"
|
||||
? PositionedDirectional(
|
||||
end: 22.w,
|
||||
bottom: 38.w,
|
||||
child: Image.asset(
|
||||
_strategy
|
||||
.getGiftPageFirstRechargeTextIcon(
|
||||
'ar',
|
||||
),
|
||||
height: 13.w,
|
||||
),
|
||||
)
|
||||
: PositionedDirectional(
|
||||
end: 16.w,
|
||||
bottom: 38.w,
|
||||
child: Image.asset(
|
||||
_strategy
|
||||
.getGiftPageFirstRechargeTextIcon(
|
||||
'en',
|
||||
),
|
||||
height: 13.w,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 34.w,
|
||||
bottom: 13.w,
|
||||
child: CountdownTimer(
|
||||
expiryDate:
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRechargeEndTime ??
|
||||
0,
|
||||
),
|
||||
color: Colors.white,
|
||||
fontSize: 12.w,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
SCDialogUtils.showFirstRechargeDialog(
|
||||
navigatorKey.currentState!.context,
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container()),
|
||||
],
|
||||
),
|
||||
_buildGiftHead(),
|
||||
if (showFirstRechargeEntry) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: _buildFirstRechargeGiftEntry(),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
],
|
||||
if (isCpTab) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
@ -1707,6 +1661,14 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
if (request.isBackpackGiftRequest) {
|
||||
unawaited(_refreshGiftBackpack());
|
||||
}
|
||||
if (!request.isBackpackGiftRequest &&
|
||||
_isInsufficientMoneyError(e, errorMessage)) {
|
||||
RoomGiftComboSendController().hide();
|
||||
RoomRechargeBottomSheet.show(
|
||||
navigatorKey.currentState?.context ?? context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
SCTts.show(errorMessage);
|
||||
}
|
||||
}
|
||||
@ -1715,21 +1677,19 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
_GiftSendRequest request,
|
||||
SocialChatUserProfileManager? profileManager,
|
||||
) {
|
||||
if (request.gift.giftTab != SCGiftType.CP.name &&
|
||||
request.gift.giftTab != "CP") {
|
||||
if (!_isCpGift(request.gift)) {
|
||||
return;
|
||||
}
|
||||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
||||
}
|
||||
|
||||
void _sendCpInviteMessageAfterGift(_GiftSendRequest request) {
|
||||
if (request.gift.giftTab != SCGiftType.CP.name &&
|
||||
request.gift.giftTab != "CP") {
|
||||
if (!_isCpGift(request.gift)) {
|
||||
return;
|
||||
}
|
||||
if (request.acceptUserIds.length != 1 || request.acceptUsers.length != 1) {
|
||||
_giftFxLog(
|
||||
'skip CP invite C2C reason=invalid_accept_count '
|
||||
'skip relation invite C2C reason=invalid_accept_count '
|
||||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
||||
);
|
||||
return;
|
||||
@ -1740,15 +1700,16 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
Future<void> _sendCpInviteMessageAfterGiftAsync(
|
||||
_GiftSendRequest request,
|
||||
) async {
|
||||
const relationType = "CP";
|
||||
final relationType = _cpRelationTypeForGift(request.gift);
|
||||
final receiverProfile = request.acceptUsers.first.user;
|
||||
final receiverId =
|
||||
(receiverProfile?.id ?? request.acceptUserIds.first).trim();
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (receiverId.isEmpty || currentProfile == null) {
|
||||
_giftFxLog(
|
||||
'skip CP invite C2C reason=missing_user '
|
||||
'receiverId=$receiverId hasCurrent=${currentProfile != null}',
|
||||
'skip relation invite C2C reason=missing_user '
|
||||
'receiverId=$receiverId relationType=$relationType '
|
||||
'hasCurrent=${currentProfile != null}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -1764,7 +1725,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
);
|
||||
if ((applyId ?? "").trim().isEmpty) {
|
||||
_giftFxLog(
|
||||
'skip CP invite C2C reason=no_apply_id receiverId=$receiverId',
|
||||
'skip relation invite C2C reason=no_apply_id '
|
||||
'receiverId=$receiverId relationType=$relationType',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -1778,6 +1740,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
account: currentProfile.account ?? currentProfile.id,
|
||||
actualAccount: currentProfile.id,
|
||||
userAvatar: currentProfile.userAvatar ?? "",
|
||||
userHeaddress: currentProfile.getHeaddress()?.sourceUrl,
|
||||
userHeaddressCover: currentProfile.getHeaddress()?.cover,
|
||||
userNickname: currentProfile.userNickname ?? "",
|
||||
relationType: relationType,
|
||||
dismissEndTime: expireAtSeconds,
|
||||
@ -1785,8 +1749,9 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
giftCount: request.quantity,
|
||||
);
|
||||
_giftFxLog(
|
||||
'send CP invite C2C applyId=$applyId '
|
||||
'receiverId=$receiverId giftId=${request.gift.id}',
|
||||
'send relation invite C2C applyId=$applyId '
|
||||
'receiverId=$receiverId relationType=$relationType '
|
||||
'giftId=${request.gift.id}',
|
||||
);
|
||||
await rtmProvider.sendCpInviteBuildMessage(
|
||||
applyId: applyId!,
|
||||
@ -1796,7 +1761,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
);
|
||||
} catch (error) {
|
||||
_giftFxLog(
|
||||
'send CP invite C2C failed receiverId=$receiverId error=$error',
|
||||
'send relation invite C2C failed '
|
||||
'receiverId=$receiverId relationType=$relationType error=$error',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1976,6 +1942,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_enqueueLowPerformanceGiftCompensation(
|
||||
gift: gift,
|
||||
@ -2102,6 +2069,33 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstRechargeGiftEntry() {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: "First recharge offer",
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _openFirstRechargeDialog,
|
||||
child: SizedBox(
|
||||
width: 355.w,
|
||||
height: 46.w,
|
||||
child: Image.asset(_firstRechargeGiftEntryCard, fit: BoxFit.fill),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openFirstRechargeDialog() {
|
||||
SmartDialog.dismiss(tag: "showGiftControl");
|
||||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||||
final navigator = navigatorKey.currentState;
|
||||
if (navigator == null || !navigator.mounted) {
|
||||
return;
|
||||
}
|
||||
SCDialogUtils.showFirstRechargeDialog(navigator.context);
|
||||
});
|
||||
}
|
||||
|
||||
void _openCpRightsGuide() {
|
||||
SmartDialog.dismiss(tag: "showGiftControl");
|
||||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/config/business_logic_strategy.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
|
||||
enum _GiftGridPageStatus { idle, loading, ready }
|
||||
|
||||
@ -447,73 +448,84 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
)
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
checkedIndex == ref.giftByTab[widget.type]!.indexOf(gift)
|
||||
? SocialChatTheme.primaryLight
|
||||
: Colors.transparent,
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: _kGiftCardVerticalPadding.w,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
netImage(
|
||||
url: gift.giftPhoto ?? "",
|
||||
fit: BoxFit.cover,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
loadingWidget: _buildGiftCoverLoading(),
|
||||
errorWidget: _buildGiftCoverNoData(),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
checkedIndex ==
|
||||
ref.giftByTab[widget.type]!.indexOf(gift)
|
||||
? SocialChatTheme.primaryLight
|
||||
: Colors.transparent,
|
||||
width: 1.w,
|
||||
),
|
||||
SizedBox(height: _kGiftIconPriceSpacing.w),
|
||||
SizedBox(
|
||||
height: _kGiftPriceRowHeight.w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
_isBackpackTab
|
||||
? [
|
||||
text(
|
||||
"x${_formatBackpackQuantity(gift)}",
|
||||
fontSize: 10,
|
||||
lineHeight: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor:
|
||||
widget.isDark
|
||||
? SocialChatTheme.primaryLight
|
||||
: SocialChatTheme.primaryColor,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Image.asset(
|
||||
_strategy.getGiftPageGoldCoinIcon(),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
text(
|
||||
"${gift.giftCandy}",
|
||||
fontSize: 10,
|
||||
lineHeight: 1,
|
||||
textColor:
|
||||
widget.isDark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: _kGiftCardVerticalPadding.w,
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
netImage(
|
||||
url: gift.giftPhoto ?? "",
|
||||
fit: BoxFit.cover,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
loadingWidget: _buildGiftCoverLoading(),
|
||||
errorWidget: _buildGiftCoverNoData(),
|
||||
),
|
||||
SizedBox(height: _kGiftIconPriceSpacing.w),
|
||||
SizedBox(
|
||||
height: _kGiftPriceRowHeight.w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
_isBackpackTab
|
||||
? [
|
||||
text(
|
||||
"x${_formatBackpackQuantity(gift)}",
|
||||
fontSize: 10,
|
||||
lineHeight: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor:
|
||||
widget.isDark
|
||||
? SocialChatTheme.primaryLight
|
||||
: SocialChatTheme.primaryColor,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Image.asset(
|
||||
_strategy.getGiftPageGoldCoinIcon(),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
text(
|
||||
"${gift.giftCandy}",
|
||||
fontSize: 10,
|
||||
lineHeight: 1,
|
||||
textColor:
|
||||
widget.isDark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_shouldShowCpRelationBadge(gift))
|
||||
PositionedDirectional(
|
||||
top: 0,
|
||||
end: 0,
|
||||
child: _buildCpRelationBadge(gift),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@ -524,6 +536,68 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldShowCpRelationBadge(SocialChatGiftRes gift) =>
|
||||
scIsCpGiftTab(widget.type) || scIsCpGift(gift);
|
||||
|
||||
Widget _buildCpRelationBadge(SocialChatGiftRes gift) {
|
||||
final spec = _cpRelationBadgeSpec(scCpRelationTypeForGift(gift));
|
||||
return Container(
|
||||
width: spec.width.w,
|
||||
height: 14.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: spec.backgroundColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(7.w),
|
||||
bottomLeft: Radius.circular(7.w),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
spec.label,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
color: spec.color,
|
||||
fontFamily: spec.fontFamily,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_CpRelationBadgeSpec _cpRelationBadgeSpec(String relationType) {
|
||||
switch (relationType) {
|
||||
case "BROTHER":
|
||||
return const _CpRelationBadgeSpec(
|
||||
label: "brother",
|
||||
width: 44,
|
||||
color: Color(0xFF024591),
|
||||
backgroundColor: Color(0xFFCEE9FF),
|
||||
fontFamily: "Source Han Sans SC",
|
||||
);
|
||||
case "SISTERS":
|
||||
return const _CpRelationBadgeSpec(
|
||||
label: "sister",
|
||||
width: 39,
|
||||
color: Color(0xFF880291),
|
||||
backgroundColor: Color(0xFFEECEFF),
|
||||
fontFamily: "Source Han Sans SC",
|
||||
);
|
||||
default:
|
||||
return const _CpRelationBadgeSpec(
|
||||
label: "cp",
|
||||
width: 26,
|
||||
color: Color(0xFFFF59B7),
|
||||
backgroundColor: Color(0xFFFFCEEA),
|
||||
fontFamily: "Inter",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isBackpackTab => widget.type == SCAppGeneralManager.backpackGiftTab;
|
||||
|
||||
String _formatBackpackQuantity(SocialChatGiftRes gift) {
|
||||
@ -557,3 +631,19 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.center, children: list);
|
||||
}
|
||||
}
|
||||
|
||||
class _CpRelationBadgeSpec {
|
||||
const _CpRelationBadgeSpec({
|
||||
required this.label,
|
||||
required this.width,
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
required this.fontFamily,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final double width;
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final String fontFamily;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/main.dart' show routeObserver;
|
||||
import 'package:yumi/main.dart' show modalRouteObserver;
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart';
|
||||
@ -24,6 +24,7 @@ import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
|
||||
@ -78,7 +79,8 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
int _shownRoomStartupFailureToken = 0;
|
||||
RtcProvider? _rtcProvider;
|
||||
bool _roomProviderRebuildScheduled = false;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
ModalRoute<dynamic>? _routeObserverRoute;
|
||||
SCRoomTopLayerGuardLease? _coveredRouteGuardLease;
|
||||
bool _roomRouteVisible = false;
|
||||
|
||||
@override
|
||||
@ -89,6 +91,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context,
|
||||
listen: false,
|
||||
).setVoiceRoomRouteVisible(true);
|
||||
SCRoomTopLayerGuard().activeListenable.addListener(
|
||||
_handleRoomTopLayerGuardChanged,
|
||||
);
|
||||
_enableRoomVisualEffects();
|
||||
_tabController.addListener(_handleTabChange);
|
||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||
@ -119,21 +124,29 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context.read<RoomMusicManager>().attachRtcProvider(rtcProvider);
|
||||
}
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
|
||||
if (route != null && _routeObserverRoute != route) {
|
||||
if (_routeObserverRoute != null) {
|
||||
routeObserver.unsubscribe(this);
|
||||
modalRouteObserver.unsubscribe(this);
|
||||
}
|
||||
_routeObserverRoute = route;
|
||||
routeObserver.subscribe(this, route);
|
||||
_setRoomRouteVisible(route.isCurrent);
|
||||
modalRouteObserver.subscribe(this, route);
|
||||
if (route.isCurrent) {
|
||||
_handleRoomRouteVisible();
|
||||
} else {
|
||||
_handleRoomRouteCovered();
|
||||
}
|
||||
}
|
||||
_ensureRoomVisualEffectsEnabled();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
SCRoomTopLayerGuard().activeListenable.removeListener(
|
||||
_handleRoomTopLayerGuardChanged,
|
||||
);
|
||||
modalRouteObserver.unsubscribe(this);
|
||||
_routeObserverRoute = null;
|
||||
_releaseCoveredRouteGuard();
|
||||
_setRoomRouteVisible(false);
|
||||
_rtcProvider?.removeListener(_handleRoomProviderChanged);
|
||||
_rtcProvider = null;
|
||||
@ -147,22 +160,52 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
_setRoomRouteVisible(true);
|
||||
_handleRoomRouteVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
_setRoomRouteVisible(true);
|
||||
_handleRoomRouteVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
_setRoomRouteVisible(false);
|
||||
_handleRoomRouteCovered();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
_handleRoomRouteCovered();
|
||||
_releaseCoveredRouteGuard();
|
||||
}
|
||||
|
||||
void _handleRoomRouteVisible() {
|
||||
_releaseCoveredRouteGuard();
|
||||
_setRoomRouteVisible(true);
|
||||
}
|
||||
|
||||
void _handleRoomRouteCovered() {
|
||||
_setRoomRouteVisible(false);
|
||||
_acquireCoveredRouteGuard();
|
||||
_clearTransientRoomVisualEffects(reason: 'voice_room_route_covered');
|
||||
}
|
||||
|
||||
void _acquireCoveredRouteGuard() {
|
||||
_coveredRouteGuardLease ??= SCRoomTopLayerGuard().acquire(
|
||||
reason: 'voice_room_route_covered',
|
||||
);
|
||||
}
|
||||
|
||||
void _releaseCoveredRouteGuard() {
|
||||
_coveredRouteGuardLease?.release();
|
||||
_coveredRouteGuardLease = null;
|
||||
}
|
||||
|
||||
void _handleRoomTopLayerGuardChanged() {
|
||||
if (!SCRoomTopLayerGuard().isActive || !mounted) {
|
||||
return;
|
||||
}
|
||||
_clearTransientRoomVisualEffects(reason: 'room_top_layer_guard');
|
||||
}
|
||||
|
||||
void _setRoomRouteVisible(bool visible) {
|
||||
@ -217,6 +260,29 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_enableRoomVisualEffects();
|
||||
}
|
||||
|
||||
void _clearTransientRoomVisualEffects({required String reason}) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
|
||||
RoomEntranceHelper.clearQueue();
|
||||
Provider.of<GiftProvider>(context, listen: false).clearAllGiftData();
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).clearActiveAnimations();
|
||||
_clearLuckyGiftComboSessions();
|
||||
_clearGiftFlightBatchDedupeTimers();
|
||||
_clearGiftVisualBatchDedupeTimers();
|
||||
_giftSeatFlightController.clear();
|
||||
OverlayManager().removeRoom(roomId: currentRoomId);
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: reason);
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
}
|
||||
|
||||
void _suspendRoomVisualEffects() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentRoomId =
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:yumi/modules/room_game/views/baishun_game_page.dart';
|
||||
import 'package:yumi/modules/room_game/views/leader_game_page.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
@ -126,6 +127,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
_launchingGameId = game.gameId;
|
||||
});
|
||||
|
||||
SCRoomTopLayerGuardLease? gameDialogGuard;
|
||||
try {
|
||||
_log(
|
||||
'launch_request roomId=$_roomId gameId=${game.gameId} '
|
||||
@ -147,23 +149,38 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
return;
|
||||
}
|
||||
|
||||
gameDialogGuard = SCRoomTopLayerGuard().acquire(reason: 'room_game_page');
|
||||
Navigator.of(context).pop();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 180));
|
||||
if (!widget.roomContext.mounted) {
|
||||
gameDialogGuard.release();
|
||||
gameDialogGuard = null;
|
||||
return;
|
||||
}
|
||||
final gamePage = _buildGamePage(game, launchModel);
|
||||
if (gamePage == null) {
|
||||
gameDialogGuard.release();
|
||||
gameDialogGuard = null;
|
||||
SCTts.show('This game provider is not wired yet');
|
||||
return;
|
||||
}
|
||||
showBottomInBottomDialog(
|
||||
widget.roomContext,
|
||||
gamePage,
|
||||
barrierColor: Colors.black54,
|
||||
barrierDismissible: true,
|
||||
);
|
||||
final activeGameDialogGuard = gameDialogGuard;
|
||||
gameDialogGuard = null;
|
||||
try {
|
||||
unawaited(
|
||||
showBottomInBottomDialog(
|
||||
widget.roomContext,
|
||||
gamePage,
|
||||
barrierColor: Colors.black54,
|
||||
barrierDismissible: true,
|
||||
).whenComplete(activeGameDialogGuard.release),
|
||||
);
|
||||
} catch (_) {
|
||||
activeGameDialogGuard.release();
|
||||
rethrow;
|
||||
}
|
||||
} catch (error) {
|
||||
gameDialogGuard?.release();
|
||||
_log('launch_error error=${_clip(error.toString(), 400)}');
|
||||
SCTts.show('Launch failed');
|
||||
} finally {
|
||||
|
||||
@ -261,17 +261,20 @@ class _BagsChatboxPageState
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
selecteChatbox = res;
|
||||
setState(() {});
|
||||
if (myChatbox?.id != res.propsResources?.id) {
|
||||
final success = await _use(res, false);
|
||||
if (!success || !mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
resource: res.propsResources,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
if (myChatbox?.id != res.propsResources?.id) {
|
||||
_use(res, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -317,31 +320,36 @@ class _BagsChatboxPageState
|
||||
);
|
||||
}
|
||||
|
||||
void _use(BagsListRes res, bool unload) {
|
||||
Future<bool> _use(BagsListRes res, bool unload) async {
|
||||
SCLoadingManager.show(context: context);
|
||||
SCStoreRepositoryImp()
|
||||
.switchPropsUse(
|
||||
SCPropsType.CHAT_BUBBLE.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
setState(() {});
|
||||
if (!unload) {
|
||||
myChatbox = res.propsResources;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfulWear);
|
||||
} else {
|
||||
myChatbox = null;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
}
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData(loadGuardCount: false);
|
||||
SCLoadingManager.hide();
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
await SCStoreRepositoryImp().switchPropsUse(
|
||||
SCPropsType.CHAT_BUBBLE.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
);
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
if (!unload) {
|
||||
myChatbox = res.propsResources;
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
myChatbox = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
setState(() {});
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,23 +273,32 @@ class BagsDataCardPageState
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
selectedDataCard = res;
|
||||
setState(() {});
|
||||
final expired =
|
||||
(res.expireTime ?? 0) < DateTime.now().millisecondsSinceEpoch;
|
||||
if (expired) {
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
resource: res.propsResources,
|
||||
previewKind: SCStoreItemPreviewKind.dataCard,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (myDataCard?.id != res.propsResources?.id) {
|
||||
final success = await _use(res, false);
|
||||
if (!success || !mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
resource: res.propsResources,
|
||||
previewKind: SCStoreItemPreviewKind.dataCard,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
final expired =
|
||||
(res.expireTime ?? 0) < DateTime.now().millisecondsSinceEpoch;
|
||||
if (expired) {
|
||||
return;
|
||||
}
|
||||
if (myDataCard?.id != res.propsResources?.id) {
|
||||
_use(res, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -332,37 +341,36 @@ class BagsDataCardPageState
|
||||
);
|
||||
}
|
||||
|
||||
void _use(BagsListRes res, bool unload) {
|
||||
Future<bool> _use(BagsListRes res, bool unload) async {
|
||||
SCLoadingManager.show(context: context);
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
SCStoreRepositoryImp()
|
||||
.switchPropsUse(
|
||||
SCPropsType.DATA_CARD.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
if (!mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
if (!unload) {
|
||||
myDataCard = res.propsResources;
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
myDataCard = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
SCLoadingManager.hide();
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
try {
|
||||
await SCStoreRepositoryImp().switchPropsUse(
|
||||
SCPropsType.DATA_CARD.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
);
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
if (!unload) {
|
||||
myDataCard = res.propsResources;
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
myDataCard = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
setState(() {});
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,18 +261,21 @@ class _BagsHeaddressPageState
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
selecteHeaddress = res;
|
||||
setState(() {});
|
||||
if (myHeaddress?.id != res.propsResources?.id) {
|
||||
final success = await _use(res, false);
|
||||
if (!success || !mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
resource: res.propsResources,
|
||||
previewKind: SCStoreItemPreviewKind.avatarFrame,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
if (myHeaddress?.id != res.propsResources?.id) {
|
||||
_use(res, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -318,31 +321,36 @@ class _BagsHeaddressPageState
|
||||
);
|
||||
}
|
||||
|
||||
void _use(BagsListRes res, bool unload) {
|
||||
Future<bool> _use(BagsListRes res, bool unload) async {
|
||||
SCLoadingManager.show(context: context);
|
||||
SCStoreRepositoryImp()
|
||||
.switchPropsUse(
|
||||
SCPropsType.AVATAR_FRAME.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
setState(() {});
|
||||
if (!unload) {
|
||||
myHeaddress = res.propsResources;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfulWear);
|
||||
} else {
|
||||
myHeaddress = null;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
}
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData(loadGuardCount: false);
|
||||
SCLoadingManager.hide();
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
await SCStoreRepositoryImp().switchPropsUse(
|
||||
SCPropsType.AVATAR_FRAME.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
);
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
if (!unload) {
|
||||
myHeaddress = res.propsResources;
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
myHeaddress = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
setState(() {});
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/bag/props_bag_mountains_detail_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
@ -246,17 +244,20 @@ class _BagsMountainsPageState
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
selecteMountains = res;
|
||||
setState(() {});
|
||||
if (myMountains?.id != res.propsResources?.id) {
|
||||
final success = await _use(res, false);
|
||||
if (!success || !mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
resource: res.propsResources,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
if (myMountains?.id != res.propsResources?.id) {
|
||||
_use(res, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -287,46 +288,36 @@ class _BagsMountainsPageState
|
||||
}
|
||||
}
|
||||
|
||||
void _showDetail(BagsListRes res) {
|
||||
SmartDialog.show(
|
||||
tag: "showPropsDetail",
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return PropsBagMountainsDetailDialog(res);
|
||||
},
|
||||
onDismiss: () {
|
||||
myMountains = AccountStorage().getMountains();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _use(BagsListRes res, bool unload) {
|
||||
Future<bool> _use(BagsListRes res, bool unload) async {
|
||||
SCLoadingManager.show(context: context);
|
||||
SCStoreRepositoryImp()
|
||||
.switchPropsUse(
|
||||
SCPropsType.RIDE.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
setState(() {});
|
||||
if (!unload) {
|
||||
myMountains = res.propsResources;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfulWear);
|
||||
} else {
|
||||
myMountains = null;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
}
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData(loadGuardCount: false);
|
||||
SCLoadingManager.hide();
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
await SCStoreRepositoryImp().switchPropsUse(
|
||||
SCPropsType.RIDE.name,
|
||||
res.propsResources?.id ?? "",
|
||||
unload,
|
||||
);
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
if (!unload) {
|
||||
myMountains = res.propsResources;
|
||||
SCTts.show(localizations.successfulWear);
|
||||
} else {
|
||||
myMountains = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
setState(() {});
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||
import 'package:yumi/ui_kit/widgets/bag/props_bag_theme_detail_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
|
||||
import 'package:yumi/modules/store/store_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -231,18 +230,21 @@ class _BagsThemePageState
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
selecteTheme = res;
|
||||
setState(() {});
|
||||
if (!(res.useTheme ?? false)) {
|
||||
final success = await _use(res, false);
|
||||
if (!success || !mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showStoreBagFullScreenResourcePreview(
|
||||
context,
|
||||
sourceUrl: res.themeBack,
|
||||
coverUrl: res.themeBack,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
if (!(res.useTheme ?? false)) {
|
||||
_use(res, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -270,62 +272,46 @@ class _BagsThemePageState
|
||||
}
|
||||
}
|
||||
|
||||
void _use(SCRoomThemeListRes res, bool unload) {
|
||||
Future<bool> _use(SCRoomThemeListRes res, bool unload) async {
|
||||
SCLoadingManager.show();
|
||||
SCStoreRepositoryImp()
|
||||
.themeSwitchUse(res.id ?? "", unload)
|
||||
.then((value) async {
|
||||
SCLoadingManager.hide();
|
||||
SmartDialog.dismiss(tag: "showPropsDetail");
|
||||
if (!unload) {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfulWear);
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).updateRoomBG(res);
|
||||
} else {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).updateRoomBG(SCRoomThemeListRes());
|
||||
selecteTheme = null;
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
}
|
||||
loadData(1);
|
||||
|
||||
///发送一条消息更新远端房间信息
|
||||
String? groupId =
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.roomAccount;
|
||||
if (groupId != null) {
|
||||
Msg msg = Msg(
|
||||
groupId: groupId,
|
||||
msg: unload ? "" : jsonEncode(res.toJson()),
|
||||
type: SCRoomMsgType.roomBGUpdate,
|
||||
);
|
||||
Provider.of<RtmProvider>(
|
||||
context!,
|
||||
listen: false,
|
||||
).dispatchMessage(msg, addLocal: false);
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
}
|
||||
|
||||
void _showDetail(SCRoomThemeListRes res) {
|
||||
SmartDialog.show(
|
||||
tag: "showPropsDetail",
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return PropsBagThemeDetailDialog(res, () {
|
||||
loadData(1);
|
||||
});
|
||||
},
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final rtcManager = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
try {
|
||||
await SCStoreRepositoryImp().themeSwitchUse(res.id ?? "", unload);
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
SmartDialog.dismiss(tag: "showPropsDetail");
|
||||
if (!unload) {
|
||||
SCTts.show(localizations.successfulWear);
|
||||
rtcManager.updateRoomBG(res);
|
||||
} else {
|
||||
rtcManager.updateRoomBG(SCRoomThemeListRes());
|
||||
selecteTheme = null;
|
||||
SCTts.show(localizations.successfullyUnloaded);
|
||||
}
|
||||
loadData(1);
|
||||
|
||||
///发送一条消息更新远端房间信息
|
||||
String? groupId =
|
||||
rtcManager.currenRoom?.roomProfile?.roomProfile?.roomAccount;
|
||||
if (groupId != null) {
|
||||
Msg msg = Msg(
|
||||
groupId: groupId,
|
||||
msg: unload ? "" : jsonEncode(res.toJson()),
|
||||
type: SCRoomMsgType.roomBGUpdate,
|
||||
);
|
||||
rtmProvider.dispatchMessage(msg, addLocal: false);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_h5_url_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
@ -40,6 +41,7 @@ class WebViewPage extends StatefulWidget {
|
||||
|
||||
class _WebViewPageState extends State<WebViewPage> {
|
||||
late WebViewController _controller;
|
||||
late final SCRoomTopLayerGuardLease _topLayerGuardLease;
|
||||
String _title = "";
|
||||
double _progress = 0.0;
|
||||
bool _isLoading = true;
|
||||
@ -48,6 +50,7 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_topLayerGuardLease = SCRoomTopLayerGuard().acquire(reason: 'webview_page');
|
||||
urlLink = SCH5UrlUtils.appendAppParams(widget.url);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[WebViewPage] load url=${_maskSensitiveUrl(urlLink)}');
|
||||
@ -177,6 +180,7 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.loadRequest(Uri.parse('about:blank'));
|
||||
_topLayerGuardLease.release();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
@ -490,7 +491,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
int? get previewRoomSeatCount => _previewRoomSeatCount;
|
||||
|
||||
bool get shouldShowRoomVisualEffects =>
|
||||
currenRoom != null && _roomVisualEffectsEnabled;
|
||||
currenRoom != null &&
|
||||
_roomVisualEffectsEnabled &&
|
||||
_voiceRoomRouteVisible &&
|
||||
!SCRoomTopLayerGuard().isActive;
|
||||
|
||||
void _startVoiceRoomForegroundService() {
|
||||
final roomProfile = currenRoom?.roomProfile?.roomProfile;
|
||||
@ -530,7 +534,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void notifyRoomVisualEffectPreferenceChanged() {
|
||||
if (!SCGlobalConfig.isEntryVehicleAnimation) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVoiceRoomRouteVisible(bool visible) {
|
||||
if (_voiceRoomRouteVisible == visible) {
|
||||
return;
|
||||
}
|
||||
_voiceRoomRouteVisible = visible;
|
||||
if (visible) {
|
||||
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
@ -538,6 +553,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_scheduleRoomRocketEntryReadyFallback(roomId);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setRoomStartupLoading() {
|
||||
@ -3178,6 +3194,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
});
|
||||
} else if (sourceUrl.isNotEmpty &&
|
||||
SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects &&
|
||||
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
@ -3236,6 +3253,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
.map((item) => item.id)
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
@ -3245,21 +3268,25 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
userProfileResolver: roomRocketRewardUserProfileForRecord,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(
|
||||
recordIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_reward_dialog',
|
||||
keepDialogTags: const <String>{_roomRocketRewardDialogTag},
|
||||
child: RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
userProfileResolver: roomRocketRewardUserProfileForRecord,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(
|
||||
recordIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
@ -3417,7 +3444,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier();
|
||||
|
||||
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
|
||||
if (shouldShowRoomVisualEffects &&
|
||||
isVoiceRoomRouteVisible &&
|
||||
!SCRoomTopLayerGuard().isActive) {
|
||||
final animationUrl =
|
||||
_firstNonBlankText([
|
||||
launch.rocketAnimationUrl,
|
||||
@ -3458,6 +3487,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
required String animationUrl,
|
||||
required RoomRocketLaunchBroadcastMessage launch,
|
||||
}) {
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
return;
|
||||
}
|
||||
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
|
||||
final topAvatarNotifier = ValueNotifier<String?>(
|
||||
_roomRocketTop1AvatarUrl(launch.safeLevel),
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
@ -896,6 +897,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: currentName,
|
||||
avatarUrl: currentProfile.userAvatar ?? "",
|
||||
headdress: currentProfile.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: currentProfile.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
receiver: receiver,
|
||||
style: RoomCpInviteDialogStyle.waiting,
|
||||
@ -920,10 +923,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: inviterName,
|
||||
avatarUrl: invite.userAvatar ?? "",
|
||||
headdress: invite.userHeaddress ?? "",
|
||||
headdressCover: invite.userHeaddressCover ?? "",
|
||||
),
|
||||
receiver: RoomCpInviteDialogUser(
|
||||
name: currentName,
|
||||
avatarUrl: currentProfile.userAvatar ?? "",
|
||||
headdress: currentProfile.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: currentProfile.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
style: RoomCpInviteDialogStyle.incoming,
|
||||
countdownSeconds: _cpInviteCountdownSeconds(invite),
|
||||
@ -1216,12 +1223,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
"actualAccount": invite.actualAccount,
|
||||
"userNickname": invite.userNickname,
|
||||
"userAvatar": invite.userAvatar,
|
||||
"userHeaddress": invite.userHeaddress,
|
||||
"userHeaddressCover": invite.userHeaddressCover,
|
||||
"acceptUserId": profile?.id,
|
||||
"acceptAccount": profile?.account,
|
||||
"acceptActualAccount": profile?.id,
|
||||
"acceptNickname": acceptName,
|
||||
"acceptUserNickname": profile?.userNickname,
|
||||
"acceptUserAvatar": profile?.userAvatar,
|
||||
"acceptHeaddress": profile?.getHeaddress()?.sourceUrl,
|
||||
"acceptHeaddressCover": profile?.getHeaddress()?.cover,
|
||||
"content": jsonEncode({...invite.toJson(), "relationType": relationType}),
|
||||
};
|
||||
_cpInviteLog(
|
||||
@ -1316,12 +1327,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
"actualAccount": invite.actualAccount,
|
||||
"userNickname": invite.userNickname,
|
||||
"userAvatar": invite.userAvatar,
|
||||
"userHeaddress": invite.userHeaddress,
|
||||
"userHeaddressCover": invite.userHeaddressCover,
|
||||
"acceptUserId": profile?.id,
|
||||
"acceptAccount": profile?.account,
|
||||
"acceptActualAccount": profile?.id,
|
||||
"acceptNickname": rejectName,
|
||||
"acceptUserNickname": profile?.userNickname,
|
||||
"acceptUserAvatar": profile?.userAvatar,
|
||||
"acceptHeaddress": profile?.getHeaddress()?.sourceUrl,
|
||||
"acceptHeaddressCover": profile?.getHeaddress()?.cover,
|
||||
"content": jsonEncode({...invite.toJson(), "relationType": relationType}),
|
||||
};
|
||||
_cpInviteLog(
|
||||
@ -1367,12 +1382,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
"actualAccount": invite.actualAccount,
|
||||
"userNickname": invite.userNickname,
|
||||
"userAvatar": invite.userAvatar,
|
||||
"userHeaddress": invite.userHeaddress,
|
||||
"userHeaddressCover": invite.userHeaddressCover,
|
||||
"acceptUserId": receiverProfile?.id ?? targetId,
|
||||
"acceptAccount": receiverProfile?.account ?? targetId,
|
||||
"acceptActualAccount": receiverProfile?.id ?? targetId,
|
||||
"acceptNickname": receiverName,
|
||||
"acceptUserNickname": receiverProfile?.userNickname,
|
||||
"acceptUserAvatar": receiverProfile?.userAvatar,
|
||||
"acceptHeaddress": receiverProfile?.getHeaddress()?.sourceUrl,
|
||||
"acceptHeaddressCover": receiverProfile?.getHeaddress()?.cover,
|
||||
"content": jsonEncode({...invite.toJson(), "relationType": relationType}),
|
||||
};
|
||||
_cpInviteLog(
|
||||
@ -1661,6 +1680,34 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
content["receiverUserAvatar"]?.toString(),
|
||||
]) ??
|
||||
"",
|
||||
headdress:
|
||||
_firstCpInviteValue([
|
||||
data["acceptHeaddress"]?.toString(),
|
||||
data["acceptAvatarFrame"]?.toString(),
|
||||
data["acceptAvatarFrameUrl"]?.toString(),
|
||||
data["receiverHeaddress"]?.toString(),
|
||||
data["receiverAvatarFrame"]?.toString(),
|
||||
data["receiverAvatarFrameUrl"]?.toString(),
|
||||
content["acceptHeaddress"]?.toString(),
|
||||
content["acceptAvatarFrame"]?.toString(),
|
||||
content["acceptAvatarFrameUrl"]?.toString(),
|
||||
content["receiverHeaddress"]?.toString(),
|
||||
content["receiverAvatarFrame"]?.toString(),
|
||||
content["receiverAvatarFrameUrl"]?.toString(),
|
||||
]) ??
|
||||
"",
|
||||
headdressCover:
|
||||
_firstCpInviteValue([
|
||||
data["acceptHeaddressCover"]?.toString(),
|
||||
data["acceptAvatarFrameCover"]?.toString(),
|
||||
data["receiverHeaddressCover"]?.toString(),
|
||||
data["receiverAvatarFrameCover"]?.toString(),
|
||||
content["acceptHeaddressCover"]?.toString(),
|
||||
content["acceptAvatarFrameCover"]?.toString(),
|
||||
content["receiverHeaddressCover"]?.toString(),
|
||||
content["receiverAvatarFrameCover"]?.toString(),
|
||||
]) ??
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
@ -3982,6 +4029,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
unawaited(
|
||||
_showRoomRocketRewardPopupDialog(
|
||||
roomId,
|
||||
attempt: attempt,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId)) {
|
||||
@ -4013,20 +4072,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
_scheduleRoomRocketPostLaunchStatusRefresh(roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(recordIds),
|
||||
);
|
||||
}
|
||||
},
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_reward_dialog',
|
||||
keepDialogTags: const <String>{_roomRocketRewardDialogTag},
|
||||
child: RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
_scheduleRoomRocketPostLaunchStatusRefresh(roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(
|
||||
recordIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
@ -4989,6 +5054,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
@ -5061,6 +5127,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
SCGiftVapSvgaManager().play(giftSourceUrl);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
rtcProvider.shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
(msg.msg?.trim().isNotEmpty ?? false)
|
||||
|
||||
@ -24,7 +24,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 状态管理变量
|
||||
bool _isAvailable = false;
|
||||
String _errorMessage = '';
|
||||
List<SelecteProductConfig> _products = [];
|
||||
final List<SelecteProductConfig> _products = [];
|
||||
List<PurchaseDetails> _purchases = [];
|
||||
|
||||
///商品列表
|
||||
@ -143,9 +143,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
_productTypeMap[product.produc.id] = 'nonConsumable'; // 默认类型
|
||||
}
|
||||
}
|
||||
|
||||
// 打印商品信息用于调试
|
||||
for (var product in _products) {}
|
||||
} catch (e) {
|
||||
SCTts.show("Failed to retrieve the product: $e");
|
||||
_errorMessage = '获取商品失败: ${e.toString()}';
|
||||
@ -163,11 +160,11 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 发起购买
|
||||
Future<void> processPurchase() async {
|
||||
ProductDetails? product;
|
||||
_products.forEach((d) {
|
||||
for (final d in _products) {
|
||||
if (d.isSelecte) {
|
||||
product = d.produc;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (product == null) {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
||||
return;
|
||||
@ -190,6 +187,76 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> processFirstRechargePurchase({
|
||||
required BuildContext context,
|
||||
required int rechargeAmountCents,
|
||||
String productPackage = '',
|
||||
}) async {
|
||||
this.context = context;
|
||||
if (!Platform.isAndroid) {
|
||||
SCTts.show('Google Play payment is only available on Android');
|
||||
return;
|
||||
}
|
||||
if (!_isAvailable || _products.isEmpty || productMap.isEmpty) {
|
||||
await initializePaymentProcessor(context);
|
||||
}
|
||||
if (!_isAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
final product = _findFirstRechargeProduct(
|
||||
rechargeAmountCents: rechargeAmountCents,
|
||||
productPackage: productPackage,
|
||||
);
|
||||
if (product == null) {
|
||||
SCTts.show('No matching Google Play product was found');
|
||||
return;
|
||||
}
|
||||
|
||||
for (final item in _products) {
|
||||
item.isSelecte = identical(item, product);
|
||||
}
|
||||
notifyListeners();
|
||||
await _goBuy(product.produc);
|
||||
}
|
||||
|
||||
SelecteProductConfig? _findFirstRechargeProduct({
|
||||
required int rechargeAmountCents,
|
||||
required String productPackage,
|
||||
}) {
|
||||
final targetProduct = productPackage.trim();
|
||||
if (targetProduct.isNotEmpty) {
|
||||
for (final product in _products) {
|
||||
if (product.produc.id == targetProduct) {
|
||||
return product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rechargeAmountCents <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final product in _products) {
|
||||
final config = productMap[product.produc.id];
|
||||
if (_matchesAmountCents(config?.unitPrice, rechargeAmountCents)) {
|
||||
return product;
|
||||
}
|
||||
if (_matchesAmountCents(product.produc.rawPrice, rechargeAmountCents)) {
|
||||
return product;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _matchesAmountCents(num? value, int targetCents) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
final cents = value >= 20 ? value.round() : (value * 100).round();
|
||||
return (cents - targetCents).abs() <= 1;
|
||||
}
|
||||
|
||||
// 处理购买结果
|
||||
void _handlePurchase(List<PurchaseDetails> purchases) {
|
||||
_purchases = purchases;
|
||||
@ -227,22 +294,29 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 新增:处理已拥有商品的情况
|
||||
Future<void> _handleAlreadyOwnedItem(PurchaseDetails purchase) async {
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
// 如果是消耗型商品,尝试消耗
|
||||
if (_getProductType(purchase.productID) == 'consumable') {
|
||||
await consumePurchase(purchase);
|
||||
// 消耗后重新获取余额
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
profileManager.balance();
|
||||
SCTts.show('outstanding purchases have been reinstated');
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
debugPrint('handle already-owned purchase failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证支付凭证
|
||||
Future<void> _verifyPayment(PurchaseDetails purchase) async {
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
// 1. 基本验证
|
||||
if (purchase.verificationData.serverVerificationData.isEmpty) {
|
||||
@ -283,14 +357,8 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 8. 更新用户余额
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
profileManager.fetchUserProfileData();
|
||||
profileManager.balance();
|
||||
|
||||
SCTts.show('purchase successful');
|
||||
} catch (e) {
|
||||
@ -316,7 +384,9 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 尝试消耗购买
|
||||
await consumePurchase(purchase);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
debugPrint('consume purchase failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _handleError(Object error, StackTrace stackTrace) {
|
||||
@ -396,7 +466,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 购买流程
|
||||
void _goBuy(ProductDetails product) async {
|
||||
Future<void> _goBuy(ProductDetails product) async {
|
||||
try {
|
||||
SCLoadingManager.show(context: context);
|
||||
notifyListeners();
|
||||
@ -442,12 +512,14 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 给一点时间处理恢复的购买
|
||||
await Future.delayed(Duration(milliseconds: 550));
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
debugPrint('check pending purchases failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加调试方法
|
||||
void logCurrentPurchaseStatus() {
|
||||
for (var purchase in _purchases) {}
|
||||
debugPrint('current purchase count: ${_purchases.length}');
|
||||
}
|
||||
|
||||
// 释放资源
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
// special : ""
|
||||
// type : ""
|
||||
// giftTab : ""
|
||||
// relationType : ""
|
||||
// standardId : 0
|
||||
// explanationGift : false
|
||||
// account : ""
|
||||
@ -26,6 +27,7 @@ class SocialChatGiftRes {
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? relationType,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
@ -48,6 +50,7 @@ class SocialChatGiftRes {
|
||||
_special = special;
|
||||
_type = type;
|
||||
_giftTab = giftTab;
|
||||
_relationType = relationType;
|
||||
_standardId = standardId;
|
||||
_jumpUrl = jumpUrl;
|
||||
_bannerUrl = bannerUrl;
|
||||
@ -110,6 +113,7 @@ class SocialChatGiftRes {
|
||||
_special = _giftResString(json['special'] ?? giftConfig?['special']);
|
||||
_type = _giftResString(json['type'] ?? giftConfig?['type']);
|
||||
_giftTab = _giftResString(json['giftTab'] ?? giftConfig?['giftTab']);
|
||||
_relationType = _giftResRelationType(json, giftConfig);
|
||||
_standardId = _giftResString(
|
||||
json['standardId'] ?? giftConfig?['standardId'],
|
||||
);
|
||||
@ -147,6 +151,7 @@ class SocialChatGiftRes {
|
||||
String? _special;
|
||||
String? _type;
|
||||
String? _giftTab;
|
||||
String? _relationType;
|
||||
String? _standardId;
|
||||
String? _jumpUrl;
|
||||
String? _bannerUrl;
|
||||
@ -169,6 +174,7 @@ class SocialChatGiftRes {
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? relationType,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
@ -191,6 +197,7 @@ class SocialChatGiftRes {
|
||||
special: special ?? _special,
|
||||
type: type ?? _type,
|
||||
giftTab: giftTab ?? _giftTab,
|
||||
relationType: relationType ?? _relationType,
|
||||
standardId: standardId ?? _standardId,
|
||||
jumpUrl: jumpUrl ?? _jumpUrl,
|
||||
bannerUrl: bannerUrl ?? _bannerUrl,
|
||||
@ -213,6 +220,7 @@ class SocialChatGiftRes {
|
||||
String? get special => _special;
|
||||
String? get type => _type;
|
||||
String? get giftTab => _giftTab;
|
||||
String? get relationType => _relationType;
|
||||
String? get standardId => _standardId;
|
||||
String? get jumpUrl => _jumpUrl;
|
||||
String? get bannerUrl => _bannerUrl;
|
||||
@ -237,6 +245,7 @@ class SocialChatGiftRes {
|
||||
map['special'] = _special;
|
||||
map['type'] = _type;
|
||||
map['giftTab'] = _giftTab;
|
||||
map['relationType'] = _relationType;
|
||||
map['standardId'] = _standardId;
|
||||
map['jumpUrl'] = _jumpUrl;
|
||||
map['bannerUrl'] = _bannerUrl;
|
||||
@ -263,6 +272,32 @@ String? _giftResString(dynamic value) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
String? _giftResRelationType(
|
||||
Map<String, dynamic> json,
|
||||
Map<String, dynamic>? giftConfig,
|
||||
) {
|
||||
const keys = <String>[
|
||||
'relationType',
|
||||
'relation_type',
|
||||
'cpRelationType',
|
||||
'cp_relation_type',
|
||||
'relationshipType',
|
||||
'relationship_type',
|
||||
'relation',
|
||||
'cpType',
|
||||
'cp_type',
|
||||
];
|
||||
for (final key in keys) {
|
||||
for (final source in <Map<String, dynamic>?>[json, giftConfig]) {
|
||||
final value = _giftResString(source?[key])?.trim();
|
||||
if (value != null && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
num? _giftResNum(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is num) return value;
|
||||
|
||||
@ -8,7 +8,9 @@ class SCCpRightsConfigRes {
|
||||
}) : levels = levels ?? const [];
|
||||
|
||||
factory SCCpRightsConfigRes.fromJson(dynamic json) {
|
||||
final source = json is Map ? json : const <String, dynamic>{};
|
||||
final rawSource = json is Map ? json : const <String, dynamic>{};
|
||||
final body = rawSource['body'] ?? rawSource['data'];
|
||||
final source = body is Map ? body : rawSource;
|
||||
final rawLevels =
|
||||
source['levels'] ?? source['levelList'] ?? source['configs'];
|
||||
return SCCpRightsConfigRes(
|
||||
|
||||
@ -0,0 +1,384 @@
|
||||
class SCFirstRechargeRewardHomeRes {
|
||||
const SCFirstRechargeRewardHomeRes({
|
||||
required this.configured,
|
||||
required this.enabled,
|
||||
required this.activityStatus,
|
||||
required this.userId,
|
||||
required this.sysOrigin,
|
||||
required this.hasRewardRecord,
|
||||
required this.rewarded,
|
||||
required this.levelConfigs,
|
||||
this.rewardStatus = '',
|
||||
this.matchedLevel,
|
||||
this.rechargeAmount = '',
|
||||
this.rechargeAmountCents = 0,
|
||||
this.rewardGroupId = '',
|
||||
this.rewardGroupName = '',
|
||||
this.rewardItems = const [],
|
||||
});
|
||||
|
||||
factory SCFirstRechargeRewardHomeRes.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCFirstRechargeRewardHomeRes(
|
||||
configured: _readBool(map['configured']),
|
||||
enabled: _readBool(map['enabled']),
|
||||
activityStatus: _readString(map['activityStatus']),
|
||||
userId: _readString(map['userId']),
|
||||
sysOrigin: _readString(map['sysOrigin']),
|
||||
hasRewardRecord: _readBool(map['hasRewardRecord']),
|
||||
rewarded: _readBool(map['rewarded']),
|
||||
levelConfigs:
|
||||
_readList(
|
||||
map['levelConfigs'],
|
||||
).map(SCFirstRechargeLevelConfig.fromJson).toList(),
|
||||
rewardStatus: _readString(map['rewardStatus']),
|
||||
matchedLevel:
|
||||
map['matchedLevel'] == null
|
||||
? null
|
||||
: SCFirstRechargeLevelConfig.fromJson(map['matchedLevel']),
|
||||
rechargeAmount: _readString(map['rechargeAmount']),
|
||||
rechargeAmountCents: _readInt(map['rechargeAmountCents']),
|
||||
rewardGroupId: _readString(map['rewardGroupId']),
|
||||
rewardGroupName: _readString(map['rewardGroupName']),
|
||||
rewardItems:
|
||||
_readList(
|
||||
map['rewardItems'],
|
||||
).map(SCFirstRechargeRewardItem.fromJson).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
final bool configured;
|
||||
final bool enabled;
|
||||
final String activityStatus;
|
||||
final String userId;
|
||||
final String sysOrigin;
|
||||
final bool hasRewardRecord;
|
||||
final bool rewarded;
|
||||
final List<SCFirstRechargeLevelConfig> levelConfigs;
|
||||
final String rewardStatus;
|
||||
final SCFirstRechargeLevelConfig? matchedLevel;
|
||||
final String rechargeAmount;
|
||||
final int rechargeAmountCents;
|
||||
final String rewardGroupId;
|
||||
final String rewardGroupName;
|
||||
final List<SCFirstRechargeRewardItem> rewardItems;
|
||||
|
||||
List<SCFirstRechargeLevelConfig> get availableLevelConfigs {
|
||||
final levels = levelConfigs.where((level) => level.enabled).toList();
|
||||
levels.sort((a, b) => a.level.compareTo(b.level));
|
||||
return levels;
|
||||
}
|
||||
|
||||
bool get shouldShowRewardDialog {
|
||||
return configured &&
|
||||
enabled &&
|
||||
activityStatus.toUpperCase() == 'ONGOING' &&
|
||||
!rewarded &&
|
||||
availableLevelConfigs.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
class SCFirstRechargeLevelConfig {
|
||||
const SCFirstRechargeLevelConfig({
|
||||
required this.id,
|
||||
required this.level,
|
||||
required this.rechargeAmount,
|
||||
required this.rechargeAmountCents,
|
||||
required this.rewardGroupId,
|
||||
required this.rewardGroupName,
|
||||
required this.rewardItems,
|
||||
required this.enabled,
|
||||
required this.reached,
|
||||
required this.current,
|
||||
this.discountText = '',
|
||||
this.totalValueText = '',
|
||||
this.productPackage = '',
|
||||
});
|
||||
|
||||
factory SCFirstRechargeLevelConfig.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCFirstRechargeLevelConfig(
|
||||
id: _readString(map['id']),
|
||||
level: _readInt(map['level']),
|
||||
rechargeAmount: _readString(map['rechargeAmount']),
|
||||
rechargeAmountCents: _readInt(map['rechargeAmountCents']),
|
||||
rewardGroupId: _readString(map['rewardGroupId']),
|
||||
rewardGroupName: _readString(map['rewardGroupName']),
|
||||
rewardItems:
|
||||
_readList(
|
||||
map['rewardItems'],
|
||||
).map(SCFirstRechargeRewardItem.fromJson).toList(),
|
||||
enabled: _readBool(map['enabled'], fallback: true),
|
||||
reached: _readBool(map['reached']),
|
||||
current: _readBool(map['current']),
|
||||
discountText: _readDiscountText(map),
|
||||
totalValueText: _readTotalValueText(map),
|
||||
productPackage: _firstString(map, const [
|
||||
'productPackage',
|
||||
'productId',
|
||||
'googleProductId',
|
||||
'googleProductPackage',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final int level;
|
||||
final String rechargeAmount;
|
||||
final int rechargeAmountCents;
|
||||
final String rewardGroupId;
|
||||
final String rewardGroupName;
|
||||
final List<SCFirstRechargeRewardItem> rewardItems;
|
||||
final bool enabled;
|
||||
final bool reached;
|
||||
final bool current;
|
||||
final String discountText;
|
||||
final String totalValueText;
|
||||
final String productPackage;
|
||||
}
|
||||
|
||||
class SCFirstRechargeRewardItem {
|
||||
const SCFirstRechargeRewardItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.content,
|
||||
required this.quantity,
|
||||
required this.cover,
|
||||
this.badgeText = '',
|
||||
this.days = 0,
|
||||
});
|
||||
|
||||
factory SCFirstRechargeRewardItem.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
final content = _readString(map['content']);
|
||||
final explicitDays = _firstInt(map, const [
|
||||
'days',
|
||||
'day',
|
||||
'durationDays',
|
||||
'validDays',
|
||||
'validityDays',
|
||||
]);
|
||||
return SCFirstRechargeRewardItem(
|
||||
id: _readString(map['id']),
|
||||
type: _readString(map['type']),
|
||||
name: _readString(map['name']),
|
||||
content: content,
|
||||
quantity: _readInt(map['quantity']),
|
||||
cover: _readString(map['cover']),
|
||||
badgeText: _firstString(map, const [
|
||||
'badgeText',
|
||||
'cornerText',
|
||||
'tagText',
|
||||
'durationText',
|
||||
'validityText',
|
||||
]),
|
||||
days: explicitDays > 0 ? explicitDays : _readDaysFromText(content),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String name;
|
||||
final String content;
|
||||
final int quantity;
|
||||
final String cover;
|
||||
final String badgeText;
|
||||
final int days;
|
||||
|
||||
bool get isCoinReward {
|
||||
final normalizedType = type.toUpperCase();
|
||||
return normalizedType.contains('COIN') ||
|
||||
normalizedType.contains('GOLD') ||
|
||||
normalizedType.contains('CANDY');
|
||||
}
|
||||
|
||||
String get displayTitle {
|
||||
if (isCoinReward && quantity > 0) {
|
||||
return '${_formatNumber(quantity)} Coins';
|
||||
}
|
||||
return _firstNonEmpty([name, content, quantity > 0 ? 'x$quantity' : '']);
|
||||
}
|
||||
|
||||
String get displayBadgeText {
|
||||
if (isCoinReward) {
|
||||
return '';
|
||||
}
|
||||
if (badgeText.trim().isNotEmpty) {
|
||||
return badgeText.trim();
|
||||
}
|
||||
if (days > 0) {
|
||||
return '${days}Days';
|
||||
}
|
||||
if (quantity > 1) {
|
||||
return 'x$quantity';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _asMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value;
|
||||
}
|
||||
if (value is Map) {
|
||||
return Map<String, dynamic>.from(value);
|
||||
}
|
||||
return const {};
|
||||
}
|
||||
|
||||
List<dynamic> _readList(dynamic value) {
|
||||
if (value is List) {
|
||||
return value;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
String _readString(dynamic value) {
|
||||
return value?.toString().trim() ?? '';
|
||||
}
|
||||
|
||||
bool _readBool(dynamic value, {bool fallback = false}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
int _readInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
if (value is String) {
|
||||
return int.tryParse(value.trim()) ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int _firstInt(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = _readInt(map[key]);
|
||||
if (value > 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String _firstString(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = _readString(map[key]);
|
||||
if (value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _firstNonEmpty(List<String> values) {
|
||||
for (final value in values) {
|
||||
final text = value.trim();
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _readDiscountText(Map<String, dynamic> map) {
|
||||
for (final key in const [
|
||||
'discountText',
|
||||
'discount',
|
||||
'discountPercent',
|
||||
'offPercent',
|
||||
'percentOff',
|
||||
'off',
|
||||
]) {
|
||||
final value = map[key];
|
||||
if (value is num) {
|
||||
final percent = value <= 1 ? (value * 100).round() : value.round();
|
||||
if (percent > 0) {
|
||||
return '$percent%';
|
||||
}
|
||||
}
|
||||
final text = _readString(value);
|
||||
if (text.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (text.contains('%')) {
|
||||
return text;
|
||||
}
|
||||
final numeric = num.tryParse(text);
|
||||
if (numeric != null) {
|
||||
final percent = numeric <= 1 ? (numeric * 100).round() : numeric.round();
|
||||
return '$percent%';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _readTotalValueText(Map<String, dynamic> map) {
|
||||
final direct = _firstString(map, const [
|
||||
'totalValueText',
|
||||
'totalRewardValueText',
|
||||
'rewardValueText',
|
||||
]);
|
||||
if (direct.isNotEmpty) {
|
||||
return direct;
|
||||
}
|
||||
for (final key in const ['totalValue', 'totalRewardValue', 'rewardValue']) {
|
||||
final value = map[key];
|
||||
if (value is num && value > 0) {
|
||||
return 'Total value of gifts: ${_formatNumber(value)} gold coins';
|
||||
}
|
||||
final text = _readString(value);
|
||||
if (text.isNotEmpty) {
|
||||
final numeric = num.tryParse(text);
|
||||
if (numeric != null) {
|
||||
return 'Total value of gifts: ${_formatNumber(numeric)} gold coins';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
int _readDaysFromText(String text) {
|
||||
final match = RegExp(
|
||||
r'(\d+)\s*(d|day|days)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
if (match == null) {
|
||||
return 0;
|
||||
}
|
||||
return int.tryParse(match.group(1) ?? '') ?? 0;
|
||||
}
|
||||
|
||||
String _formatNumber(num value) {
|
||||
final text = value.toInt().toString();
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
final remaining = text.length - i;
|
||||
buffer.write(text[i]);
|
||||
if (remaining > 1 && remaining % 3 == 1) {
|
||||
buffer.write(',');
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
@ -1,104 +1,142 @@
|
||||
/// account : ""
|
||||
/// countryCode : ""
|
||||
/// countryName : ""
|
||||
/// userAvatar : ""
|
||||
/// userNickname : ""
|
||||
/// actualAccount : ""
|
||||
|
||||
class SCSystemInvitMessageRes {
|
||||
SCSystemInvitMessageRes({
|
||||
String? account,
|
||||
String? countryCode,
|
||||
String? countryName,
|
||||
String? userAvatar,
|
||||
String? userNickname,
|
||||
String? applyType,
|
||||
String? content,
|
||||
String? actualAccount,
|
||||
String? relationType,
|
||||
int? dismissEndTime,
|
||||
String? giftCover,
|
||||
int? giftCount,
|
||||
}) {
|
||||
_account = account;
|
||||
_countryCode = countryCode;
|
||||
_countryName = countryName;
|
||||
_userAvatar = userAvatar;
|
||||
_userNickname = userNickname;
|
||||
_actualAccount = actualAccount;
|
||||
_applyType = applyType;
|
||||
_content = content;
|
||||
_relationType = relationType;
|
||||
_dismissEndTime = dismissEndTime;
|
||||
_giftCover = giftCover;
|
||||
_giftCount = giftCount;
|
||||
}
|
||||
|
||||
SCSystemInvitMessageRes.fromJson(dynamic json) {
|
||||
_account = json['account'];
|
||||
_countryCode = json['countryCode'];
|
||||
_countryName = json['countryName'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_userNickname = json['userNickname'];
|
||||
_actualAccount = json['actualAccount'];
|
||||
_applyType = json['applyType'];
|
||||
_content = json['content'];
|
||||
_relationType = json['relationType'];
|
||||
_dismissEndTime = _asInt(json['dismissEndTime']);
|
||||
_giftCover = json['giftCover'];
|
||||
_giftCount = _asInt(json['giftCount']);
|
||||
}
|
||||
|
||||
static int? _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
String? _account;
|
||||
String? _countryCode;
|
||||
String? _countryName;
|
||||
String? _userAvatar;
|
||||
String? _userNickname;
|
||||
String? _actualAccount;
|
||||
String? _applyType;
|
||||
String? _content;
|
||||
String? _relationType;
|
||||
int? _dismissEndTime;
|
||||
String? _giftCover;
|
||||
int? _giftCount;
|
||||
|
||||
String? get account => _account;
|
||||
String? get countryCode => _countryCode;
|
||||
String? get countryName => _countryName;
|
||||
String? get userAvatar => _userAvatar;
|
||||
String? get userNickname => _userNickname;
|
||||
String? get actualAccount => _actualAccount;
|
||||
String? get applyType => _applyType;
|
||||
String? get content => _content;
|
||||
String? get relationType => _relationType;
|
||||
int? get dismissEndTime => _dismissEndTime;
|
||||
String? get giftCover => _giftCover;
|
||||
int? get giftCount => _giftCount;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['account'] = _account;
|
||||
map['countryCode'] = _countryCode;
|
||||
map['countryName'] = _countryName;
|
||||
map['userAvatar'] = _userAvatar;
|
||||
map['userNickname'] = _userNickname;
|
||||
map['actualAccount'] = _actualAccount;
|
||||
map['applyType'] = _applyType;
|
||||
map['content'] = _content;
|
||||
map['relationType'] = _relationType;
|
||||
map['dismissEndTime'] = _dismissEndTime;
|
||||
map['giftCover'] = _giftCover;
|
||||
map['giftCount'] = _giftCount;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
// account : ""
|
||||
// countryCode : ""
|
||||
// countryName : ""
|
||||
// userAvatar : ""
|
||||
// userNickname : ""
|
||||
// actualAccount : ""
|
||||
|
||||
class SCSystemInvitMessageRes {
|
||||
SCSystemInvitMessageRes({
|
||||
String? account,
|
||||
String? countryCode,
|
||||
String? countryName,
|
||||
String? userAvatar,
|
||||
String? userHeaddress,
|
||||
String? userHeaddressCover,
|
||||
String? userNickname,
|
||||
String? applyType,
|
||||
String? content,
|
||||
String? actualAccount,
|
||||
String? relationType,
|
||||
int? dismissEndTime,
|
||||
String? giftCover,
|
||||
int? giftCount,
|
||||
}) {
|
||||
_account = account;
|
||||
_countryCode = countryCode;
|
||||
_countryName = countryName;
|
||||
_userAvatar = userAvatar;
|
||||
_userHeaddress = userHeaddress;
|
||||
_userHeaddressCover = userHeaddressCover;
|
||||
_userNickname = userNickname;
|
||||
_actualAccount = actualAccount;
|
||||
_applyType = applyType;
|
||||
_content = content;
|
||||
_relationType = relationType;
|
||||
_dismissEndTime = dismissEndTime;
|
||||
_giftCover = giftCover;
|
||||
_giftCount = giftCount;
|
||||
}
|
||||
|
||||
SCSystemInvitMessageRes.fromJson(dynamic json) {
|
||||
_account = json['account'];
|
||||
_countryCode = json['countryCode'];
|
||||
_countryName = json['countryName'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_userHeaddress = _firstNonBlank(json, const [
|
||||
'userHeaddress',
|
||||
'userAvatarFrame',
|
||||
'userAvatarFrameUrl',
|
||||
'headdress',
|
||||
'headdressUrl',
|
||||
'avatarFrame',
|
||||
'avatarFrameUrl',
|
||||
]);
|
||||
_userHeaddressCover = _firstNonBlank(json, const [
|
||||
'userHeaddressCover',
|
||||
'userAvatarFrameCover',
|
||||
'headdressCover',
|
||||
'avatarFrameCover',
|
||||
]);
|
||||
_userNickname = json['userNickname'];
|
||||
_actualAccount = json['actualAccount'];
|
||||
_applyType = json['applyType'];
|
||||
_content = json['content'];
|
||||
_relationType = json['relationType'];
|
||||
_dismissEndTime = _asInt(json['dismissEndTime']);
|
||||
_giftCover = json['giftCover'];
|
||||
_giftCount = _asInt(json['giftCount']);
|
||||
}
|
||||
|
||||
static int? _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
String? _account;
|
||||
String? _countryCode;
|
||||
String? _countryName;
|
||||
String? _userAvatar;
|
||||
String? _userHeaddress;
|
||||
String? _userHeaddressCover;
|
||||
String? _userNickname;
|
||||
String? _actualAccount;
|
||||
String? _applyType;
|
||||
String? _content;
|
||||
String? _relationType;
|
||||
int? _dismissEndTime;
|
||||
String? _giftCover;
|
||||
int? _giftCount;
|
||||
|
||||
String? get account => _account;
|
||||
String? get countryCode => _countryCode;
|
||||
String? get countryName => _countryName;
|
||||
String? get userAvatar => _userAvatar;
|
||||
String? get userHeaddress => _userHeaddress;
|
||||
String? get userHeaddressCover => _userHeaddressCover;
|
||||
String? get userNickname => _userNickname;
|
||||
String? get actualAccount => _actualAccount;
|
||||
String? get applyType => _applyType;
|
||||
String? get content => _content;
|
||||
String? get relationType => _relationType;
|
||||
int? get dismissEndTime => _dismissEndTime;
|
||||
String? get giftCover => _giftCover;
|
||||
int? get giftCount => _giftCount;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['account'] = _account;
|
||||
map['countryCode'] = _countryCode;
|
||||
map['countryName'] = _countryName;
|
||||
map['userAvatar'] = _userAvatar;
|
||||
map['userHeaddress'] = _userHeaddress;
|
||||
map['userHeaddressCover'] = _userHeaddressCover;
|
||||
map['userNickname'] = _userNickname;
|
||||
map['actualAccount'] = _actualAccount;
|
||||
map['applyType'] = _applyType;
|
||||
map['content'] = _content;
|
||||
map['relationType'] = _relationType;
|
||||
map['dismissEndTime'] = _dismissEndTime;
|
||||
map['giftCover'] = _giftCover;
|
||||
map['giftCount'] = _giftCount;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
String? _firstNonBlank(dynamic json, List<String> keys) {
|
||||
if (json is! Map) {
|
||||
return null;
|
||||
}
|
||||
for (final key in keys) {
|
||||
final text = json[key]?.toString().trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
|
||||
@ -46,6 +47,9 @@ abstract class SocialChatConfigRepository {
|
||||
///获取商品配置
|
||||
Future<List<SCProductConfigRes>> productConfig();
|
||||
|
||||
///首充奖励首页
|
||||
Future<SCFirstRechargeRewardHomeRes> firstRechargeRewardHome();
|
||||
|
||||
///谷歌内购
|
||||
Future<SCGooglePayRes> googlePay(
|
||||
String product,
|
||||
|
||||
@ -30,6 +30,7 @@ class OverlayManager {
|
||||
static const Duration _rocketFloatingFallbackDedupWindow = Duration(
|
||||
seconds: 30,
|
||||
);
|
||||
static const int _maxStackedFloatingVisibleCount = 2;
|
||||
static const List<int> _stackedFloatingOrder = [5, 1, 4, 3];
|
||||
static const double _stackedFloatingBaseTop = 70;
|
||||
static const double _stackedFloatingGap = 8;
|
||||
@ -331,7 +332,9 @@ class OverlayManager {
|
||||
bool _canPlayMessage(SCFloatingMessage message) {
|
||||
final slotKey = _stackSlotKeyFor(message);
|
||||
if (slotKey != null) {
|
||||
return !_isPlaying && !_stackedOverlayEntries.containsKey(slotKey);
|
||||
return !_isPlaying &&
|
||||
!_stackedOverlayEntries.containsKey(slotKey) &&
|
||||
_stackedOverlayEntries.length < _maxStackedFloatingVisibleCount;
|
||||
}
|
||||
return !_isPlaying && _stackedOverlayEntries.isEmpty;
|
||||
}
|
||||
@ -507,29 +510,12 @@ class OverlayManager {
|
||||
if (previousMessage != null) {
|
||||
visualTop +=
|
||||
_stackedVisualHeightFor(previousMessage) + _stackedFloatingGap;
|
||||
} else if (_shouldReserveStackedSlot(message, key)) {
|
||||
visualTop += _stackedReservedVisualHeightFor(key) + _stackedFloatingGap;
|
||||
}
|
||||
}
|
||||
final wrapperTop = visualTop - _stackedInternalTopFor(message);
|
||||
return wrapperTop < 0 ? 0 : wrapperTop;
|
||||
}
|
||||
|
||||
bool _shouldReserveStackedSlot(SCFloatingMessage message, int slotKey) {
|
||||
return _homeRootTabsVisible &&
|
||||
_isRegionRoomRocketMessage(message) &&
|
||||
slotKey == 4;
|
||||
}
|
||||
|
||||
double _stackedReservedVisualHeightFor(int slotKey) {
|
||||
switch (slotKey) {
|
||||
case 4:
|
||||
return 66;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
double _stackedVisualHeightFor(SCFloatingMessage message) {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
@ -167,6 +168,17 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///go/app/first-recharge-reward/home
|
||||
@override
|
||||
Future<SCFirstRechargeRewardHomeRes> firstRechargeRewardHome() async {
|
||||
final result = await http.get<SCFirstRechargeRewardHomeRes>(
|
||||
"/go/app/first-recharge-reward/home",
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCFirstRechargeRewardHomeRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///order/purchase-pay/google
|
||||
@override
|
||||
Future<SCGooglePayRes> googlePay(
|
||||
|
||||
@ -1,52 +1,73 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_h5_url_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
|
||||
import '../../shared/data_sources/models/enum/sc_banner_open_type.dart';
|
||||
|
||||
class SCBannerUtils {
|
||||
static Future<void> openBanner(
|
||||
SCIndexBannerRes item,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (item.content == SCBannerOpenType.ENTER_ROOM.name) {
|
||||
var params = item.params;
|
||||
if (params != null && params.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(params, context);
|
||||
}
|
||||
} else {
|
||||
var params = item.params;
|
||||
if (params == null || params.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCUrlLauncherUtils.shouldOpenExternally(params)) {
|
||||
final launched = await SCUrlLauncherUtils.launchExternal(params);
|
||||
if (launched) {
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (SCStringUtils.checkIfUrl(params)) {
|
||||
final h5Url = _appendToken(params);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(h5Url)}&showTitle=false",
|
||||
replace: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _appendToken(String url) {
|
||||
return SCH5UrlUtils.appendToken(url);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/modules/webview/webview_page.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_h5_url_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
|
||||
import '../../shared/data_sources/models/enum/sc_banner_open_type.dart';
|
||||
|
||||
class SCBannerUtils {
|
||||
static Future<void> openBanner(
|
||||
SCIndexBannerRes item,
|
||||
BuildContext context, {
|
||||
bool openH5AsRoomPopup = false,
|
||||
}) async {
|
||||
if (item.content == SCBannerOpenType.ENTER_ROOM.name) {
|
||||
var params = item.params;
|
||||
if (params != null && params.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(params, context);
|
||||
}
|
||||
} else {
|
||||
var params = item.params;
|
||||
if (params == null || params.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCUrlLauncherUtils.shouldOpenExternally(params)) {
|
||||
final launched = await SCUrlLauncherUtils.launchExternal(params);
|
||||
if (launched) {
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (SCStringUtils.checkIfUrl(params)) {
|
||||
final h5Url = _appendToken(params);
|
||||
if (openH5AsRoomPopup) {
|
||||
await _showRoomH5Popup(context, h5Url);
|
||||
return;
|
||||
}
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(h5Url)}&showTitle=false",
|
||||
replace: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _appendToken(String url) {
|
||||
return SCH5UrlUtils.appendToken(url);
|
||||
}
|
||||
|
||||
static Future<void> _showRoomH5Popup(BuildContext context, String h5Url) {
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
return showBottomInBottomDialog(
|
||||
context,
|
||||
SizedBox(
|
||||
height: screenHeight * 0.75,
|
||||
width: double.infinity,
|
||||
child: WebViewPage(url: h5Url, showTitle: "false"),
|
||||
),
|
||||
barrierColor: Colors.black54,
|
||||
barrierDismissible: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
81
lib/shared/tools/sc_cp_gift_relation_utils.dart
Normal file
@ -0,0 +1,81 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
|
||||
bool scIsCpGiftTab(String? tabType) =>
|
||||
tabType?.trim().toUpperCase() == SCGiftType.CP.name;
|
||||
|
||||
bool scIsCpGift(SocialChatGiftRes? gift) => scIsCpGiftTab(gift?.giftTab);
|
||||
|
||||
String scCpRelationTypeForGift(SocialChatGiftRes gift) {
|
||||
final explicit = _normalizeExplicitGiftRelationType(gift.relationType);
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
for (final value in <String?>[
|
||||
gift.type,
|
||||
gift.giftCode,
|
||||
gift.giftName,
|
||||
gift.special,
|
||||
gift.standardId,
|
||||
]) {
|
||||
final inferred = _inferCpRelationTypeFromGiftText(value);
|
||||
if (inferred != null) {
|
||||
return inferred;
|
||||
}
|
||||
}
|
||||
return "CP";
|
||||
}
|
||||
|
||||
String? _normalizeExplicitGiftRelationType(String? relationType) {
|
||||
final raw = relationType?.trim();
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final normalized = scNormalizeCpRelationType(raw);
|
||||
if (normalized != "CP" || _isCpRelationText(raw)) {
|
||||
return normalized;
|
||||
}
|
||||
return _inferCpRelationTypeFromGiftText(raw) ?? normalized;
|
||||
}
|
||||
|
||||
bool _isCpRelationText(String value) {
|
||||
final upper = value.trim().toUpperCase();
|
||||
return upper == "CP" || upper == "COUPLE";
|
||||
}
|
||||
|
||||
String? _inferCpRelationTypeFromGiftText(String? value) {
|
||||
final upper = value?.trim().toUpperCase();
|
||||
if (upper == null || upper.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (_hasRelationToken(upper, const ["SIS", "SISTER", "SISTERS"])) {
|
||||
return "SISTERS";
|
||||
}
|
||||
if (_hasRelationToken(upper, const [
|
||||
"BRO",
|
||||
"BROS",
|
||||
"BROTHER",
|
||||
"BROTHERS",
|
||||
"SWORN_BRO",
|
||||
"SWORN_BROS",
|
||||
"SWORN_BROTHER",
|
||||
"SWORN_BROTHERS",
|
||||
])) {
|
||||
return "BROTHER";
|
||||
}
|
||||
if (_hasRelationToken(upper, const ["CP", "COUPLE"])) {
|
||||
return "CP";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _hasRelationToken(String upperText, List<String> tokens) {
|
||||
for (final token in tokens) {
|
||||
final escaped = RegExp.escape(token);
|
||||
if (RegExp('(^|[^A-Z0-9])$escaped([^A-Z0-9]|\$)').hasMatch(upperText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -1,11 +1,16 @@
|
||||
String scNormalizeCpRelationType(String? relationType) {
|
||||
final value = relationType?.trim().toUpperCase() ?? "";
|
||||
switch (value) {
|
||||
case "BRO":
|
||||
case "BROS":
|
||||
case "BROTHER":
|
||||
case "BROTHERS":
|
||||
case "SWORN_BRO":
|
||||
case "SWORN_BROS":
|
||||
case "SWORN_BROTHER":
|
||||
case "SWORN_BROTHERS":
|
||||
return "BROTHER";
|
||||
case "SIS":
|
||||
case "SISTER":
|
||||
case "SISTERS":
|
||||
return "SISTERS";
|
||||
|
||||
@ -6,237 +6,15 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/banner/index_banner_page.dart';
|
||||
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
|
||||
import 'package:yumi/ui_kit/widgets/first_recharge/first_recharge_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
|
||||
class SCDialogUtils {
|
||||
static showFirstRechargeDialog(BuildContext context) {
|
||||
SmartDialog.show(
|
||||
tag: "showFirstRecharge",
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 530.w,
|
||||
width: ScreenUtil().screenWidth * 0.86,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"sc_images/index/sc_icon_first_recharge_bg.png",
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 138.w),
|
||||
GestureDetector(
|
||||
child: Row(
|
||||
textDirection: TextDirection.ltr,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_my_rechage_title.png",
|
||||
height: 25.w,
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(-3.w, 0),
|
||||
child: text(
|
||||
"${AccountStorage().getCurrentUser()?.userProfile?.firstRechargeAmount}",
|
||||
textColor: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
SCRoomUtils.closeAllDialogs();
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
WalletRoute.recharge,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 55.w),
|
||||
(AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRechargeAmount ??
|
||||
0) <
|
||||
0.99
|
||||
? Container(height: 60.w)
|
||||
: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 28.w,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_claimed_text.png",
|
||||
width: 70.w,
|
||||
height: 35.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 40.w),
|
||||
(AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRechargeAmount ??
|
||||
0) <
|
||||
4.99
|
||||
? Container(height: 60.w)
|
||||
: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 28.w,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_claimed_text.png",
|
||||
width: 70.w,
|
||||
height: 35.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 48.w),
|
||||
(AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRechargeAmount ??
|
||||
0) <
|
||||
9.99
|
||||
? Container(height: 60.w)
|
||||
: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 28.w,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
height: 60.w,
|
||||
width: 60.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_claimed_text.png",
|
||||
width: 70.w,
|
||||
height: 35.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
bottom: 7.w,
|
||||
child: CountdownTimer(
|
||||
expiryDate: DateTime.fromMillisecondsSinceEpoch(
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.firstRechargeEndTime ??
|
||||
0,
|
||||
),
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
static Future<void> showFirstRechargeDialog(BuildContext context) {
|
||||
return FirstRechargeDialog.showFromRemote(context);
|
||||
}
|
||||
|
||||
static void showDynamicCommentOptDialog(
|
||||
@ -276,7 +54,7 @@ class SCDialogUtils {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
reportCallback?.call();
|
||||
reportCallback.call();
|
||||
SmartDialog.dismiss(tag: "showDynamicCommentOptDialog");
|
||||
},
|
||||
),
|
||||
@ -297,7 +75,7 @@ class SCDialogUtils {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
deleteCallback?.call();
|
||||
deleteCallback.call();
|
||||
SmartDialog.dismiss(tag: "showDynamicCommentOptDialog");
|
||||
},
|
||||
),
|
||||
|
||||
@ -516,6 +516,10 @@ class SCGiftVapSvgaManager {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (SCRoomEffectScheduler().isSuppressed) {
|
||||
_log('play ignored because room effects are suppressed path=$path');
|
||||
return;
|
||||
}
|
||||
final resolvedPriority = resolveEffectPriority(
|
||||
priority: priority,
|
||||
type: type,
|
||||
|
||||
@ -19,9 +19,29 @@ class SCRoomEffectScheduler {
|
||||
Timer? _deferredDrainTimer;
|
||||
int _pendingHighCostTaskCount = 0;
|
||||
int _nextTaskId = 0;
|
||||
int _suppressedCount = 0;
|
||||
|
||||
bool get hasActiveHighCostEffects => _pendingHighCostTaskCount > 0;
|
||||
|
||||
bool get isSuppressed => _suppressedCount > 0;
|
||||
|
||||
void beginSuppressEffects({String reason = 'unknown'}) {
|
||||
_suppressedCount += 1;
|
||||
clearDeferredTasks(reason: reason);
|
||||
clearHighCostTasks(reason: reason);
|
||||
_log('begin_suppress reason=$reason count=$_suppressedCount');
|
||||
}
|
||||
|
||||
void endSuppressEffects({String reason = 'unknown'}) {
|
||||
if (_suppressedCount > 0) {
|
||||
_suppressedCount -= 1;
|
||||
}
|
||||
_log('end_suppress reason=$reason count=$_suppressedCount');
|
||||
if (!isSuppressed) {
|
||||
_scheduleDeferredDrainIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
void registerHighCostTaskQueued({String? debugLabel}) {
|
||||
_pendingHighCostTaskCount += 1;
|
||||
_cancelDeferredDrain();
|
||||
@ -59,6 +79,10 @@ class SCRoomEffectScheduler {
|
||||
required String debugLabel,
|
||||
required VoidCallback action,
|
||||
}) {
|
||||
if (isSuppressed) {
|
||||
_log('drop_suppressed task=$debugLabel');
|
||||
return;
|
||||
}
|
||||
if (!hasActiveHighCostEffects) {
|
||||
action();
|
||||
return;
|
||||
@ -108,7 +132,12 @@ class SCRoomEffectScheduler {
|
||||
);
|
||||
try {
|
||||
task.action();
|
||||
} catch (error, stackTrace) {}
|
||||
} catch (error, stackTrace) {
|
||||
_log(
|
||||
'deferred task failed task=${task.debugLabel} id=${task.id} '
|
||||
'error=$error stack=$stackTrace',
|
||||
);
|
||||
}
|
||||
|
||||
_scheduleDeferredDrainIfNeeded();
|
||||
}
|
||||
|
||||
183
lib/shared/tools/sc_room_top_layer_guard.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
|
||||
|
||||
class SCRoomTopLayerGuard {
|
||||
static const int _maxDeferredActions = 16;
|
||||
static const String _redPacketOpenDialogTag = 'showRoomRedPacketOpenDialog';
|
||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
static const String _roomRocketPagLaunchDialogTag =
|
||||
'showRoomRocketPagLaunchDialog';
|
||||
|
||||
static final SCRoomTopLayerGuard _instance = SCRoomTopLayerGuard._internal();
|
||||
|
||||
factory SCRoomTopLayerGuard() => _instance;
|
||||
|
||||
SCRoomTopLayerGuard._internal();
|
||||
|
||||
final Queue<VoidCallback> _deferredActions = Queue<VoidCallback>();
|
||||
final ValueNotifier<bool> _activeListenable = ValueNotifier<bool>(false);
|
||||
int _activeCount = 0;
|
||||
|
||||
bool get isActive => _activeCount > 0;
|
||||
|
||||
ValueListenable<bool> get activeListenable => _activeListenable;
|
||||
|
||||
SCRoomTopLayerGuardLease acquire({
|
||||
required String reason,
|
||||
Set<String> keepDialogTags = const <String>{},
|
||||
}) {
|
||||
_begin(reason, keepDialogTags: keepDialogTags);
|
||||
return SCRoomTopLayerGuardLease._(this, reason);
|
||||
}
|
||||
|
||||
void runWhenInactive(VoidCallback action) {
|
||||
if (!isActive) {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
while (_deferredActions.length >= _maxDeferredActions) {
|
||||
_deferredActions.removeFirst();
|
||||
}
|
||||
_deferredActions.add(action);
|
||||
}
|
||||
|
||||
void _begin(String reason, {Set<String> keepDialogTags = const <String>{}}) {
|
||||
_activeCount += 1;
|
||||
if (_activeCount != 1) {
|
||||
return;
|
||||
}
|
||||
_activeListenable.value = true;
|
||||
OverlayManager().beginSuppressFloatingScreens(reason: reason);
|
||||
SCRoomEffectScheduler().beginSuppressEffects(reason: reason);
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: reason);
|
||||
RoomEntranceHelper.clearQueue();
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
SCGiftVapSvgaManager().pauseAnim();
|
||||
if (!keepDialogTags.contains(_redPacketOpenDialogTag)) {
|
||||
unawaited(SmartDialog.dismiss(tag: _redPacketOpenDialogTag));
|
||||
}
|
||||
if (!keepDialogTags.contains(_roomRocketRewardDialogTag)) {
|
||||
unawaited(SmartDialog.dismiss(tag: _roomRocketRewardDialogTag));
|
||||
}
|
||||
if (!keepDialogTags.contains(_roomRocketPagLaunchDialogTag)) {
|
||||
unawaited(SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag));
|
||||
}
|
||||
}
|
||||
|
||||
void _release(String reason) {
|
||||
if (_activeCount <= 0) {
|
||||
return;
|
||||
}
|
||||
_activeCount -= 1;
|
||||
if (_activeCount != 0) {
|
||||
return;
|
||||
}
|
||||
OverlayManager().endSuppressFloatingScreens(reason: reason);
|
||||
SCRoomEffectScheduler().endSuppressEffects(reason: reason);
|
||||
SCGiftVapSvgaManager().resumeAnim();
|
||||
_activeListenable.value = false;
|
||||
_drainDeferredActions();
|
||||
}
|
||||
|
||||
void _drainDeferredActions() {
|
||||
if (_deferredActions.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final actions = List<VoidCallback>.from(_deferredActions);
|
||||
_deferredActions.clear();
|
||||
scheduleMicrotask(() {
|
||||
for (final action in actions) {
|
||||
if (isActive) {
|
||||
runWhenInactive(action);
|
||||
continue;
|
||||
}
|
||||
action();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomTopLayerSuppressor extends StatefulWidget {
|
||||
const SCRoomTopLayerSuppressor({
|
||||
super.key,
|
||||
required this.reason,
|
||||
required this.child,
|
||||
this.keepDialogTags = const <String>{},
|
||||
});
|
||||
|
||||
final String reason;
|
||||
final Widget child;
|
||||
final Set<String> keepDialogTags;
|
||||
|
||||
@override
|
||||
State<SCRoomTopLayerSuppressor> createState() =>
|
||||
_SCRoomTopLayerSuppressorState();
|
||||
}
|
||||
|
||||
class _SCRoomTopLayerSuppressorState extends State<SCRoomTopLayerSuppressor> {
|
||||
SCRoomTopLayerGuardLease? _lease;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_acquire();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SCRoomTopLayerSuppressor oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.reason == widget.reason &&
|
||||
setEquals(oldWidget.keepDialogTags, widget.keepDialogTags)) {
|
||||
return;
|
||||
}
|
||||
_release();
|
||||
_acquire();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_release();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _acquire() {
|
||||
_lease = SCRoomTopLayerGuard().acquire(
|
||||
reason: widget.reason,
|
||||
keepDialogTags: widget.keepDialogTags,
|
||||
);
|
||||
}
|
||||
|
||||
void _release() {
|
||||
_lease?.release();
|
||||
_lease = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomTopLayerGuardLease {
|
||||
SCRoomTopLayerGuardLease._(this._guard, this.reason);
|
||||
|
||||
final SCRoomTopLayerGuard _guard;
|
||||
final String reason;
|
||||
bool _released = false;
|
||||
|
||||
void release() {
|
||||
if (_released) {
|
||||
return;
|
||||
}
|
||||
_released = true;
|
||||
_guard._release(reason);
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
|
||||
typedef SCRoomUtils = SCChatRoomHelper;
|
||||
|
||||
@ -185,73 +186,76 @@ class SCChatRoomHelper {
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return Container(
|
||||
width: ScreenUtil().screenWidth * 0.75,
|
||||
height: 120.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 8.w),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
child: Icon(Icons.close, color: Colors.black, size: 20.w),
|
||||
onTap: () {
|
||||
SmartDialog.dismiss(tag: "showGoToRecharge");
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_recharge_prompt_dialog',
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth * 0.75,
|
||||
height: 120.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 8.w),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
child: Icon(Icons.close, color: Colors.black, size: 20.w),
|
||||
onTap: () {
|
||||
SmartDialog.dismiss(tag: "showGoToRecharge");
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: text(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.insufhcientGoldsGoToRecharge,
|
||||
textColor: Colors.black,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 38.w,
|
||||
width: 125.w,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFFFEB219), Color(0xFFFF9326)],
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||
),
|
||||
child: text(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.insufhcientGoldsGoToRecharge,
|
||||
textColor: Colors.black,
|
||||
textAlign: TextAlign.center,
|
||||
SCAppLocalizations.of(context)!.goToRecharge,
|
||||
textColor: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 38.w,
|
||||
width: 125.w,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFFFEB219), Color(0xFFFF9326)],
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||
),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.goToRecharge,
|
||||
textColor: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
onTap: () {
|
||||
closeAllDialogs();
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
WalletRoute.recharge,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
closeAllDialogs();
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
WalletRoute.recharge,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
944
lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart
Normal file
@ -0,0 +1,944 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/services/payment/google_payment_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
|
||||
enum FirstRechargeRewardStyle { coin, prop }
|
||||
|
||||
class FirstRechargeReward {
|
||||
const FirstRechargeReward({
|
||||
required this.title,
|
||||
required this.style,
|
||||
this.badgeText = '',
|
||||
this.cover = '',
|
||||
});
|
||||
|
||||
final String title;
|
||||
final FirstRechargeRewardStyle style;
|
||||
final String badgeText;
|
||||
final String cover;
|
||||
|
||||
bool get isCoinReward => style == FirstRechargeRewardStyle.coin;
|
||||
}
|
||||
|
||||
class FirstRechargePackage {
|
||||
const FirstRechargePackage({
|
||||
required this.priceText,
|
||||
required this.rewards,
|
||||
this.discountText = '',
|
||||
this.totalValueText = '',
|
||||
this.rechargeAmountCents = 0,
|
||||
this.productPackage = '',
|
||||
});
|
||||
|
||||
final String priceText;
|
||||
final String discountText;
|
||||
final String totalValueText;
|
||||
final int rechargeAmountCents;
|
||||
final String productPackage;
|
||||
final List<FirstRechargeReward> rewards;
|
||||
}
|
||||
|
||||
class FirstRechargeDialogData {
|
||||
const FirstRechargeDialogData({
|
||||
required this.packages,
|
||||
this.timeRemainingText = '',
|
||||
this.initialPackageIndex = 0,
|
||||
});
|
||||
|
||||
final List<FirstRechargePackage> packages;
|
||||
final String timeRemainingText;
|
||||
final int initialPackageIndex;
|
||||
|
||||
factory FirstRechargeDialogData.mock() {
|
||||
return const FirstRechargeDialogData(
|
||||
timeRemainingText: 'Time remaining: 6 D 59 H 59 M 59S',
|
||||
packages: [
|
||||
FirstRechargePackage(
|
||||
priceText: r'$0.99',
|
||||
discountText: '70%',
|
||||
totalValueText: 'Total value of gifts: xxx gold coins',
|
||||
rewards: [
|
||||
FirstRechargeReward(
|
||||
title: 'NameName',
|
||||
badgeText: '80000',
|
||||
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'NameName',
|
||||
badgeText: '240000',
|
||||
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: '240,000 Coins',
|
||||
cover: _FirstRechargeAssets.rewardCoinStack,
|
||||
style: FirstRechargeRewardStyle.coin,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Vehicle',
|
||||
badgeText: '3Days',
|
||||
cover: _FirstRechargeAssets.rewardVehicle,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Frame',
|
||||
badgeText: '3Days',
|
||||
cover: _FirstRechargeAssets.rewardFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Gift',
|
||||
badgeText: 'x1',
|
||||
cover: _FirstRechargeAssets.rewardGift,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
],
|
||||
),
|
||||
FirstRechargePackage(
|
||||
priceText: r'$2.99',
|
||||
discountText: '60%',
|
||||
totalValueText: 'Total value of gifts: 720,000 gold coins',
|
||||
rewards: [
|
||||
FirstRechargeReward(
|
||||
title: 'Avatar Frame',
|
||||
badgeText: '7Days',
|
||||
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Room Frame',
|
||||
badgeText: '7Days',
|
||||
cover: _FirstRechargeAssets.rewardFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: '720,000 Coins',
|
||||
cover: _FirstRechargeAssets.rewardCoinStack,
|
||||
style: FirstRechargeRewardStyle.coin,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Vehicle',
|
||||
badgeText: '7Days',
|
||||
cover: _FirstRechargeAssets.rewardVehicle,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Gift',
|
||||
badgeText: 'x3',
|
||||
cover: _FirstRechargeAssets.rewardGift,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
FirstRechargeReward(
|
||||
title: 'Profile Frame',
|
||||
badgeText: '7Days',
|
||||
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
factory FirstRechargeDialogData.fromRewardHome(
|
||||
SCFirstRechargeRewardHomeRes home,
|
||||
) {
|
||||
final levels = home.availableLevelConfigs.take(2).toList();
|
||||
final packages =
|
||||
levels.map((level) {
|
||||
return FirstRechargePackage(
|
||||
priceText: _formatPriceText(level),
|
||||
discountText: level.discountText,
|
||||
totalValueText: level.totalValueText,
|
||||
rechargeAmountCents: level.rechargeAmountCents,
|
||||
productPackage: level.productPackage,
|
||||
rewards:
|
||||
level.rewardItems.map((item) {
|
||||
return FirstRechargeReward(
|
||||
title: item.displayTitle,
|
||||
style:
|
||||
item.isCoinReward
|
||||
? FirstRechargeRewardStyle.coin
|
||||
: FirstRechargeRewardStyle.prop,
|
||||
badgeText: item.displayBadgeText,
|
||||
cover: item.cover,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final currentIndex = levels.indexWhere((level) => level.current);
|
||||
return FirstRechargeDialogData(
|
||||
packages: packages,
|
||||
initialPackageIndex: currentIndex >= 0 ? currentIndex : 0,
|
||||
);
|
||||
}
|
||||
|
||||
static String _formatPriceText(SCFirstRechargeLevelConfig level) {
|
||||
final amount = level.rechargeAmount.trim();
|
||||
if (amount.isNotEmpty) {
|
||||
return amount.startsWith(r'$') ? amount : '\$$amount';
|
||||
}
|
||||
if (level.rechargeAmountCents > 0) {
|
||||
return '\$${(level.rechargeAmountCents / 100).toStringAsFixed(2)}';
|
||||
}
|
||||
return r'$0.00';
|
||||
}
|
||||
}
|
||||
|
||||
class FirstRechargeDialog extends StatefulWidget {
|
||||
const FirstRechargeDialog({super.key, required this.data, this.onBuyNow});
|
||||
|
||||
static const String dialogTag = 'showFirstRechargeDialog';
|
||||
static const double _designWidth = 340;
|
||||
static const double _designHeight = 528;
|
||||
|
||||
final FirstRechargeDialogData data;
|
||||
final ValueChanged<FirstRechargePackage>? onBuyNow;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required FirstRechargeDialogData data,
|
||||
ValueChanged<FirstRechargePackage>? onBuyNow,
|
||||
}) async {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
await SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
clickMaskDismiss: true,
|
||||
maskColor: Colors.black.withValues(alpha: 0.5),
|
||||
builder: (_) => FirstRechargeDialog(data: data, onBuyNow: onBuyNow),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showFromRemote(BuildContext context) async {
|
||||
try {
|
||||
SCLoadingManager.show(context: context);
|
||||
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (!home.shouldShowRewardDialog) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
return;
|
||||
}
|
||||
final data = FirstRechargeDialogData.fromRewardHome(home);
|
||||
if (data.packages.isEmpty) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.hide();
|
||||
await show(
|
||||
context,
|
||||
data: data,
|
||||
onBuyNow: (package) {
|
||||
_startGooglePay(context, package);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
SCTts.show('Failed to load first recharge rewards');
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _startGooglePay(
|
||||
BuildContext context,
|
||||
FirstRechargePackage package,
|
||||
) async {
|
||||
if (!Platform.isAndroid) {
|
||||
SCTts.show('Google Play payment is only available on Android');
|
||||
return;
|
||||
}
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
final processor = Provider.of<AndroidPaymentProcessor>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
await processor.processFirstRechargePurchase(
|
||||
context: context,
|
||||
rechargeAmountCents: package.rechargeAmountCents,
|
||||
productPackage: package.productPackage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<FirstRechargeDialog> createState() => _FirstRechargeDialogState();
|
||||
}
|
||||
|
||||
class _FirstRechargeDialogState extends State<FirstRechargeDialog> {
|
||||
late int _selectedPackageIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedPackageIndex = widget.data.initialPackageIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FirstRechargeDialog oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.data != widget.data) {
|
||||
_selectedPackageIndex = _clampedPackageIndex(_selectedPackageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.data.packages.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final dialogWidth = math.min(
|
||||
screenWidth - 35.w,
|
||||
FirstRechargeDialog._designWidth.w,
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 11.w),
|
||||
child: SizedBox(
|
||||
width: dialogWidth,
|
||||
child: AspectRatio(
|
||||
aspectRatio:
|
||||
FirstRechargeDialog._designWidth /
|
||||
FirstRechargeDialog._designHeight,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final scale =
|
||||
constraints.maxWidth / FirstRechargeDialog._designWidth;
|
||||
final selectedPackage =
|
||||
widget.data.packages[_clampedPackageIndex(
|
||||
_selectedPackageIndex,
|
||||
)];
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_positionedImage(
|
||||
asset: _FirstRechargeAssets.panelTop,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 340,
|
||||
height: 219,
|
||||
scale: scale,
|
||||
),
|
||||
_positionedImage(
|
||||
asset: _FirstRechargeAssets.panelMiddle,
|
||||
left: 0,
|
||||
top: 219,
|
||||
width: 340,
|
||||
height: 166,
|
||||
scale: scale,
|
||||
),
|
||||
_positionedImage(
|
||||
asset: _FirstRechargeAssets.panelBottom,
|
||||
left: 0,
|
||||
top: 351,
|
||||
width: 340,
|
||||
height: 177,
|
||||
scale: scale,
|
||||
),
|
||||
_positionedImage(
|
||||
asset: _FirstRechargeAssets.title,
|
||||
left: 45,
|
||||
top: 26,
|
||||
width: 246,
|
||||
height: 116,
|
||||
scale: scale,
|
||||
),
|
||||
Positioned(
|
||||
top: 146 * scale,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _SingleLineText(
|
||||
widget.data.timeRemainingText,
|
||||
height: 12 * scale,
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 50 * scale,
|
||||
top: 168 * scale,
|
||||
width: 240 * scale,
|
||||
height: 31 * scale,
|
||||
child: _PriceSelector(
|
||||
packages: widget.data.packages,
|
||||
selectedIndex: _clampedPackageIndex(
|
||||
_selectedPackageIndex,
|
||||
),
|
||||
scale: scale,
|
||||
onSelected: (index) {
|
||||
setState(() {
|
||||
_selectedPackageIndex = index;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
..._buildRewardCards(selectedPackage.rewards, scale),
|
||||
Positioned(
|
||||
top: 425 * scale,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _SingleLineText(
|
||||
selectedPackage.totalValueText,
|
||||
height: 12 * scale,
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 70 * scale,
|
||||
top: 447 * scale,
|
||||
width: 200 * scale,
|
||||
height: 42 * scale,
|
||||
child: SCDebounceWidget(
|
||||
onTap: () => widget.onBuyNow?.call(selectedPackage),
|
||||
child: Image.asset(
|
||||
_FirstRechargeAssets.buyNowButton,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selectedPackage.discountText.trim().isNotEmpty)
|
||||
Positioned(
|
||||
left: 238 * scale,
|
||||
top: 435 * scale,
|
||||
width: 38 * scale,
|
||||
height: 38 * scale,
|
||||
child: _DiscountBadge(
|
||||
text: selectedPackage.discountText,
|
||||
scale: scale,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 294 * scale,
|
||||
top: 14 * scale,
|
||||
width: 44 * scale,
|
||||
height: 44 * scale,
|
||||
child: SCDebounceWidget(
|
||||
onTap:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: FirstRechargeDialog.dialogTag,
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
_FirstRechargeAssets.close,
|
||||
width: 24 * scale,
|
||||
height: 25 * scale,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _clampedPackageIndex(int value) {
|
||||
return value.clamp(0, widget.data.packages.length - 1).toInt();
|
||||
}
|
||||
|
||||
List<Widget> _buildRewardCards(
|
||||
List<FirstRechargeReward> rewards,
|
||||
double scale,
|
||||
) {
|
||||
const positions = [
|
||||
Offset(20, 211),
|
||||
Offset(124, 211),
|
||||
Offset(227, 211),
|
||||
Offset(20, 316),
|
||||
Offset(124, 316),
|
||||
Offset(227, 316),
|
||||
];
|
||||
|
||||
return List<Widget>.generate(positions.length, (index) {
|
||||
final reward =
|
||||
index < rewards.length
|
||||
? rewards[index]
|
||||
: const FirstRechargeReward(
|
||||
title: '',
|
||||
style: FirstRechargeRewardStyle.prop,
|
||||
);
|
||||
final position = positions[index];
|
||||
return Positioned(
|
||||
left: position.dx * scale,
|
||||
top: position.dy * scale,
|
||||
width: 93 * scale,
|
||||
height: 97 * scale,
|
||||
child: _RewardCard(reward: reward, scale: scale),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _positionedImage({
|
||||
required String asset,
|
||||
required double left,
|
||||
required double top,
|
||||
required double width,
|
||||
required double height,
|
||||
required double scale,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: left * scale,
|
||||
top: top * scale,
|
||||
width: width * scale,
|
||||
height: height * scale,
|
||||
child: Image.asset(asset, fit: BoxFit.fill),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PriceSelector extends StatelessWidget {
|
||||
const _PriceSelector({
|
||||
required this.packages,
|
||||
required this.selectedIndex,
|
||||
required this.scale,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<FirstRechargePackage> packages;
|
||||
final int selectedIndex;
|
||||
final double scale;
|
||||
final ValueChanged<int> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visiblePackages = packages.take(2).toList();
|
||||
final normalizedSelectedIndex = selectedIndex.clamp(0, 1).toInt();
|
||||
final selectedIsTrailing = normalizedSelectedIndex == 1;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
_FirstRechargeAssets.priceTabsBg,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
left: (selectedIsTrailing ? 120 : 5) * scale,
|
||||
top: 2 * scale,
|
||||
width: (selectedIsTrailing ? 120 : 110) * scale,
|
||||
height: 27 * scale,
|
||||
child: _PriceSelectedBackground(
|
||||
scale: scale,
|
||||
trailing: selectedIsTrailing,
|
||||
),
|
||||
),
|
||||
for (var index = 0; index < visiblePackages.length; index++)
|
||||
Positioned(
|
||||
left: (index == 0 ? 5 : 125) * scale,
|
||||
top: 7 * scale,
|
||||
width: 110 * scale,
|
||||
height: 16 * scale,
|
||||
child: SCDebounceWidget(
|
||||
onTap: () => onSelected(index),
|
||||
child: _SingleLineText(
|
||||
visiblePackages[index].priceText,
|
||||
height: 16 * scale,
|
||||
fontSize: 14 * scale,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
selectedIndex == index
|
||||
? Colors.white
|
||||
: const Color(0xFF6F4C2B),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PriceSelectedBackground extends StatelessWidget {
|
||||
const _PriceSelectedBackground({required this.scale, required this.trailing});
|
||||
|
||||
final double scale;
|
||||
final bool trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final texture = SizedBox(
|
||||
width: 188.25 * scale,
|
||||
height: 39.52 * scale,
|
||||
child: Image.asset(
|
||||
_FirstRechargeAssets.priceSelectedTexture,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
|
||||
return ClipPath(
|
||||
clipper: _PriceSelectionClipper(trailing: trailing),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
Positioned(
|
||||
left: trailing ? null : -19.58 * scale,
|
||||
right: trailing ? -19.58 * scale : null,
|
||||
top: -5.37 * scale,
|
||||
child:
|
||||
trailing
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.diagonal3Values(-1.0, 1.0, 1.0),
|
||||
child: texture,
|
||||
)
|
||||
: texture,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PriceSelectionClipper extends CustomClipper<Path> {
|
||||
const _PriceSelectionClipper({required this.trailing});
|
||||
|
||||
final bool trailing;
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final w = size.width;
|
||||
final h = size.height;
|
||||
final curveX = w * (11.2769 / 110);
|
||||
final centerY = h * (13.3043 / 27);
|
||||
|
||||
if (trailing) {
|
||||
return Path()
|
||||
..moveTo(0, h)
|
||||
..lineTo(0, 0)
|
||||
..lineTo(w - curveX, 0)
|
||||
..cubicTo(
|
||||
w - w * (7.51795 / 110),
|
||||
h * (0.521738 / 27),
|
||||
w,
|
||||
h * (3.91303 / 27),
|
||||
w,
|
||||
centerY,
|
||||
)
|
||||
..cubicTo(
|
||||
w,
|
||||
h * (22.6956 / 27),
|
||||
w - w * (7.51795 / 110),
|
||||
h * (26.3478 / 27),
|
||||
w - curveX,
|
||||
h,
|
||||
)
|
||||
..lineTo(0, h)
|
||||
..close();
|
||||
}
|
||||
|
||||
return Path()
|
||||
..moveTo(w, h)
|
||||
..lineTo(w, 0)
|
||||
..lineTo(curveX, 0)
|
||||
..cubicTo(
|
||||
w * (7.51795 / 110),
|
||||
h * (0.521738 / 27),
|
||||
0,
|
||||
h * (3.91303 / 27),
|
||||
0,
|
||||
centerY,
|
||||
)
|
||||
..cubicTo(
|
||||
0,
|
||||
h * (22.6956 / 27),
|
||||
w * (7.51795 / 110),
|
||||
h * (26.3478 / 27),
|
||||
curveX,
|
||||
h,
|
||||
)
|
||||
..lineTo(w, h)
|
||||
..close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant _PriceSelectionClipper oldClipper) {
|
||||
return oldClipper.trailing != trailing;
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardCard extends StatelessWidget {
|
||||
const _RewardCard({required this.reward, required this.scale});
|
||||
|
||||
final FirstRechargeReward reward;
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
reward.isCoinReward
|
||||
? _FirstRechargeAssets.rewardCardCoin
|
||||
: _FirstRechargeAssets.rewardCard,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
if (!reward.isCoinReward && reward.badgeText.trim().isNotEmpty)
|
||||
Positioned(
|
||||
left: 8 * scale,
|
||||
top: 3 * scale,
|
||||
width: 42 * scale,
|
||||
height: 10 * scale,
|
||||
child: _SingleLineText(
|
||||
reward.badgeText,
|
||||
height: 10 * scale,
|
||||
fontSize: 8 * scale,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 17 * scale,
|
||||
top: 14 * scale,
|
||||
width: 59 * scale,
|
||||
height: 59 * scale,
|
||||
child: _RewardCover(reward: reward, scale: scale),
|
||||
),
|
||||
Positioned(
|
||||
left: 5 * scale,
|
||||
right: 5 * scale,
|
||||
top: 75 * scale,
|
||||
height: 15 * scale,
|
||||
child: _SingleLineText(
|
||||
reward.title,
|
||||
height: 15 * scale,
|
||||
fontSize: 12 * scale,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF6F4C2B),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardCover extends StatelessWidget {
|
||||
const _RewardCover({required this.reward, required this.scale});
|
||||
|
||||
final FirstRechargeReward reward;
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cover = reward.cover.trim();
|
||||
if (cover.startsWith('http://') || cover.startsWith('https://')) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: cover,
|
||||
fit: BoxFit.contain,
|
||||
placeholder: (_, __) => _placeholder(),
|
||||
errorWidget: (_, __, ___) => _placeholder(),
|
||||
);
|
||||
}
|
||||
|
||||
if (cover.isNotEmpty) {
|
||||
return Image.asset(
|
||||
cover,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _placeholder(),
|
||||
);
|
||||
}
|
||||
|
||||
return _placeholder();
|
||||
}
|
||||
|
||||
Widget _placeholder() {
|
||||
if (reward.isCoinReward) {
|
||||
return Image.asset(
|
||||
_FirstRechargeAssets.yumiCoin,
|
||||
width: 42 * scale,
|
||||
height: 42 * scale,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 48 * scale,
|
||||
height: 48 * scale,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFE5A8), Color(0xFFFFB04F)],
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
size: 28 * scale,
|
||||
color: const Color(0xFF6F4C2B),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiscountBadge extends StatelessWidget {
|
||||
const _DiscountBadge({required this.text, required this.scale});
|
||||
|
||||
final String text;
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
_FirstRechargeAssets.discountBadge,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 5 * scale,
|
||||
right: 5 * scale,
|
||||
top: 8 * scale,
|
||||
height: 11 * scale,
|
||||
child: _SingleLineText(
|
||||
text,
|
||||
height: 11 * scale,
|
||||
fontSize: 10 * scale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFFFCF0B7),
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: const Color(0xFF01378F).withValues(alpha: 0.89),
|
||||
offset: Offset(0, 1 * scale),
|
||||
blurRadius: 1 * scale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 10 * scale,
|
||||
right: 10 * scale,
|
||||
top: 20 * scale,
|
||||
height: 9 * scale,
|
||||
child: _SingleLineText(
|
||||
'off',
|
||||
height: 9 * scale,
|
||||
fontSize: 8 * scale,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFFFCF0B7),
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: const Color(0xFF01378F).withValues(alpha: 0.89),
|
||||
offset: Offset(0, 1 * scale),
|
||||
blurRadius: 1 * scale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SingleLineText extends StatelessWidget {
|
||||
const _SingleLineText(
|
||||
this.text, {
|
||||
required this.height,
|
||||
required this.fontSize,
|
||||
required this.fontWeight,
|
||||
required this.color,
|
||||
this.alignment = Alignment.center,
|
||||
this.shadows,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double height;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final Color color;
|
||||
final Alignment alignment;
|
||||
final List<Shadow>? shadows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: alignment,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
textAlign:
|
||||
alignment == Alignment.centerLeft
|
||||
? TextAlign.left
|
||||
: TextAlign.center,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
letterSpacing: 0,
|
||||
shadows: shadows,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FirstRechargeAssets {
|
||||
static const String panelTop = 'sc_images/first_recharge/panel_top.png';
|
||||
static const String panelMiddle = 'sc_images/first_recharge/panel_middle.png';
|
||||
static const String panelBottom = 'sc_images/first_recharge/panel_bottom.png';
|
||||
static const String close = 'sc_images/first_recharge/close.png';
|
||||
static const String title = 'sc_images/first_recharge/title.png';
|
||||
static const String priceTabsBg =
|
||||
'sc_images/first_recharge/price_tabs_bg.png';
|
||||
static const String priceSelectedTexture =
|
||||
'sc_images/first_recharge/price_selected_texture.png';
|
||||
static const String rewardCard = 'sc_images/first_recharge/reward_card.png';
|
||||
static const String rewardCardCoin =
|
||||
'sc_images/first_recharge/reward_card_coin.png';
|
||||
static const String rewardAvatarFrame =
|
||||
'sc_images/first_recharge/reward_avatar_frame_1.png';
|
||||
static const String rewardVehicle =
|
||||
'sc_images/first_recharge/reward_vehicle.png';
|
||||
static const String rewardFrame = 'sc_images/first_recharge/reward_frame.png';
|
||||
static const String rewardCoinStack =
|
||||
'sc_images/first_recharge/reward_coin_stack.png';
|
||||
static const String rewardGift = 'sc_images/first_recharge/reward_gift.png';
|
||||
static const String buyNowButton =
|
||||
'sc_images/first_recharge/buy_now_button.png';
|
||||
static const String discountBadge =
|
||||
'sc_images/first_recharge/discount_badge.png';
|
||||
static const String yumiCoin = 'sc_images/register_reward/yumi_coin.png';
|
||||
}
|
||||
@ -66,6 +66,7 @@ class _SCGiftComboSendButtonState extends State<SCGiftComboSendButton> {
|
||||
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: 32.w,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: DecoratedBox(
|
||||
@ -99,19 +100,21 @@ class _SCGiftComboSendButtonState extends State<SCGiftComboSendButton> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.w,
|
||||
horizontal: 20.w,
|
||||
),
|
||||
child: Text(
|
||||
widget.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -236,6 +236,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
}
|
||||
|
||||
bool get _effectsEnabled =>
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -256,7 +257,9 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectsEnabled = context.select<RtcProvider, bool>(
|
||||
(rtcProvider) => rtcProvider.shouldShowRoomVisualEffects,
|
||||
(rtcProvider) =>
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
rtcProvider.shouldShowRoomVisualEffects,
|
||||
);
|
||||
if (!effectsEnabled) {
|
||||
if (_animationQueue.isNotEmpty || _isQueueProcessing) {
|
||||
|
||||
@ -14,10 +14,17 @@ enum RoomCpInviteDialogStyle { waiting, incoming }
|
||||
enum RoomCpInviteActionState { pending, expired, rejected, accepted }
|
||||
|
||||
class RoomCpInviteDialogUser {
|
||||
const RoomCpInviteDialogUser({required this.name, this.avatarUrl = ''});
|
||||
const RoomCpInviteDialogUser({
|
||||
required this.name,
|
||||
this.avatarUrl = '',
|
||||
this.headdress = '',
|
||||
this.headdressCover = '',
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String avatarUrl;
|
||||
final String headdress;
|
||||
final String headdressCover;
|
||||
}
|
||||
|
||||
class RoomCpInviteDialog extends StatefulWidget {
|
||||
@ -391,7 +398,6 @@ class _RoomCpInviteDialogState extends State<RoomCpInviteDialog> {
|
||||
width: 108.w,
|
||||
height: 34.w,
|
||||
color: Colors.white,
|
||||
leadingPadding: 22.w,
|
||||
onTap: _handleAccept,
|
||||
),
|
||||
],
|
||||
@ -540,7 +546,7 @@ class _PositionedAvatar extends StatelessWidget {
|
||||
Positioned(
|
||||
left: avatarLeft,
|
||||
top: avatarTop,
|
||||
child: ClipOval(child: _AvatarImage(user: user)),
|
||||
child: _AvatarImage(user: user),
|
||||
),
|
||||
Positioned(
|
||||
left: left,
|
||||
@ -565,20 +571,13 @@ class _AvatarImage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (user.avatarUrl.trim().isNotEmpty) {
|
||||
return netImage(
|
||||
url: user.avatarUrl,
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
defaultImg: 'sc_images/general/sc_icon_avar_defalt.png',
|
||||
);
|
||||
}
|
||||
return Image.asset(
|
||||
'sc_images/general/sc_icon_avar_defalt.png',
|
||||
return head(
|
||||
url: user.avatarUrl,
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
headdress: user.headdress,
|
||||
headdressCover: user.headdressCover,
|
||||
showDefault: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -729,7 +728,6 @@ class _ActionButton extends StatelessWidget {
|
||||
required this.height,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
this.leadingPadding = 0,
|
||||
});
|
||||
|
||||
final String asset;
|
||||
@ -738,7 +736,6 @@ class _ActionButton extends StatelessWidget {
|
||||
final double height;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
final double leadingPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -751,18 +748,20 @@ class _ActionButton extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(asset, width: width, height: height, fit: BoxFit.fill),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: leadingPadding),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -22,18 +21,17 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
final num index;
|
||||
final SocialChatUserProfile? clickUser;
|
||||
|
||||
const EmptyMaiSelect({Key? key, required this.index, this.clickUser})
|
||||
: super(key: key);
|
||||
const EmptyMaiSelect({super.key, required this.index, this.clickUser});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RtcProvider provider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
List<Widget> list = [];
|
||||
list.add(SizedBox(height: 10.w));
|
||||
if (index == -1) {
|
||||
if (provider.isFz() || provider.isGL()) {
|
||||
if (clickUser?.id !=
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
if (clickUser?.id != currentUserId) {
|
||||
list.add(
|
||||
_item(
|
||||
SCAppLocalizations.of(context)!.inviteToTheMicrophone,
|
||||
@ -71,8 +69,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
}
|
||||
} else {
|
||||
MicRes? userWheat = provider.micAtIndexForDisplay(index);
|
||||
if (userWheat?.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
if (userWheat?.user?.id == currentUserId) {
|
||||
if (provider.isFz()) {
|
||||
///点击的是自己
|
||||
if (userWheat?.micMute ?? false) {
|
||||
@ -213,6 +210,28 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
final targetUserId = userWheat?.user?.id ?? clickUser?.id;
|
||||
final isTargetRoomManager =
|
||||
clickUser?.roles == SCRoomRolesType.HOMEOWNER.name ||
|
||||
clickUser?.roles == SCRoomRolesType.ADMIN.name;
|
||||
final canKickOffMic =
|
||||
targetUserId != null &&
|
||||
targetUserId.isNotEmpty &&
|
||||
targetUserId != currentUserId &&
|
||||
(provider.isFz() || (provider.isGL() && !isTargetRoomManager));
|
||||
if (canKickOffMic) {
|
||||
list.add(
|
||||
_item(
|
||||
SCAppLocalizations.of(context)!.removeTheMic,
|
||||
"sc_images/room/sc_icon_mic_leavel.png",
|
||||
context,
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
provider.killXiaMai(targetUserId);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (userWheat?.user == null) {
|
||||
if (!(userWheat?.micLock ?? false)) {
|
||||
@ -247,7 +266,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
context,
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
showBottomInBottomDialog(
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomUserInfoCard(userId: clickUser?.id),
|
||||
);
|
||||
@ -271,7 +290,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
color: Color(0xff09372E).withOpacity(0.5),
|
||||
color: Color(0xff09372E).withValues(alpha: 0.5),
|
||||
child: Column(children: list),
|
||||
),
|
||||
),
|
||||
@ -296,7 +315,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
decoration:
|
||||
msg == SCAppLocalizations.of(context)!.cancel
|
||||
? BoxDecoration(
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.all(Radius.circular(32.w)),
|
||||
)
|
||||
: null,
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.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_tts.dart';
|
||||
@ -47,6 +48,22 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
List<RoomRedPacketUiData> dataList, {
|
||||
String? initialPacketId,
|
||||
}) {
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
final deferredContext = context;
|
||||
final deferredDataList = List<RoomRedPacketUiData>.from(dataList);
|
||||
final deferredInitialPacketId = initialPacketId;
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
if (!deferredContext.mounted) {
|
||||
return;
|
||||
}
|
||||
showList(
|
||||
deferredContext,
|
||||
deferredDataList,
|
||||
initialPacketId: deferredInitialPacketId,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final normalizedList = dataList
|
||||
.map(withLocalClaimState)
|
||||
.where((item) => item.hasPacketId)
|
||||
@ -82,10 +99,14 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
maskColor: Colors.black.withValues(alpha: 0.55),
|
||||
clickMaskDismiss: true,
|
||||
builder:
|
||||
(_) => RoomRedPacketOpenDialog(
|
||||
data: packets[initialIndex],
|
||||
dataList: packets,
|
||||
initialIndex: initialIndex,
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_red_packet_open_dialog',
|
||||
keepDialogTags: const <String>{RoomRedPacketOpenDialog.dialogTag},
|
||||
child: RoomRedPacketOpenDialog(
|
||||
data: packets[initialIndex],
|
||||
dataList: packets,
|
||||
initialIndex: initialIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.d
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.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/widgets/room/room_msg_item.dart';
|
||||
@ -54,7 +55,11 @@ class RoomRedPacketSendPanel extends StatefulWidget {
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => const RoomRedPacketSendPanel(),
|
||||
builder:
|
||||
(_) => const SCRoomTopLayerSuppressor(
|
||||
reason: 'room_red_packet_send_panel',
|
||||
child: RoomRedPacketSendPanel(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -841,7 +846,11 @@ class _RoomRedPacketRechargeSheet extends StatefulWidget {
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black.withValues(alpha: 0.45),
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => const _RoomRedPacketRechargeSheet(),
|
||||
builder:
|
||||
(_) => const SCRoomTopLayerSuppressor(
|
||||
reason: 'room_red_packet_recharge_sheet',
|
||||
child: _RoomRedPacketRechargeSheet(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
@ -103,11 +104,14 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketDialogLoader(
|
||||
roomId: roomId,
|
||||
navigator: navigator,
|
||||
initialStatus: status,
|
||||
onClose: () => SmartDialog.dismiss(tag: dialogTag),
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_dialog',
|
||||
child: _RoomRocketDialogLoader(
|
||||
roomId: roomId,
|
||||
navigator: navigator,
|
||||
initialStatus: status,
|
||||
onClose: () => SmartDialog.dismiss(tag: dialogTag),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -857,12 +861,15 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketRecordDialogLoader(
|
||||
roomId: widget.roomId,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.recordDialogTag,
|
||||
),
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_record_dialog',
|
||||
child: _RoomRocketRecordDialogLoader(
|
||||
roomId: widget.roomId,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.recordDialogTag,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -879,12 +886,15 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketNoteDialogLoader(
|
||||
initialSections: _noteSections,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.noteDialogTag,
|
||||
),
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_note_dialog',
|
||||
child: _RoomRocketNoteDialogLoader(
|
||||
initialSections: _noteSections,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.noteDialogTag,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
205
lib/ui_kit/widgets/room/room_animation_switch_dialog.dart
Normal file
@ -0,0 +1,205 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RoomAnimationSwitchDialog extends StatefulWidget {
|
||||
const RoomAnimationSwitchDialog({super.key});
|
||||
|
||||
static const String tag = "showRoomAnimationSwitchDialog";
|
||||
|
||||
static void show(BuildContext context) {
|
||||
SmartDialog.show(
|
||||
tag: tag,
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
debounce: true,
|
||||
clickMaskDismiss: true,
|
||||
maskColor: Colors.transparent,
|
||||
builder:
|
||||
(_) => const SCRoomTopLayerSuppressor(
|
||||
reason: 'room_animation_switch_dialog',
|
||||
child: RoomAnimationSwitchDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomAnimationSwitchDialog> createState() =>
|
||||
_RoomAnimationSwitchDialogState();
|
||||
}
|
||||
|
||||
class _RoomAnimationSwitchDialogState extends State<RoomAnimationSwitchDialog> {
|
||||
late bool _giftEffectsEnabled;
|
||||
late bool _entryEffectsEnabled;
|
||||
|
||||
String? get _account =>
|
||||
AccountStorage().getCurrentUser()?.userProfile?.account;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_giftEffectsEnabled = SCGlobalConfig.isGiftSpecialEffects;
|
||||
_entryEffectsEnabled = SCGlobalConfig.isEntryVehicleAnimation;
|
||||
}
|
||||
|
||||
Future<void> _setGiftEffectsEnabled(bool enabled) async {
|
||||
final next = SCGlobalConfig.clampVisualEffectPreference(enabled);
|
||||
setState(() {
|
||||
_giftEffectsEnabled = next;
|
||||
});
|
||||
SCGlobalConfig.isGiftSpecialEffects = next;
|
||||
SCGlobalConfig.isLuckGiftSpecialEffects = next;
|
||||
await Future.wait([
|
||||
DataPersistence.setBool("$_account-GiftSpecialEffects", next),
|
||||
DataPersistence.setBool("$_account-LuckGiftSpecialEffects", next),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _setEntryEffectsEnabled(bool enabled) async {
|
||||
final next = SCGlobalConfig.clampVisualEffectPreference(enabled);
|
||||
setState(() {
|
||||
_entryEffectsEnabled = next;
|
||||
});
|
||||
SCGlobalConfig.isEntryVehicleAnimation = next;
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).notifyRoomVisualEffectPreferenceChanged();
|
||||
await DataPersistence.setBool("$_account-EntryVehicleAnimation", next);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 151.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff072121),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12.w),
|
||||
topRight: Radius.circular(12.w),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 16.w,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: text(
|
||||
"Animation",
|
||||
fontSize: 18,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
lineHeight: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
_buildSwitchRow(
|
||||
top: 59.w,
|
||||
title: "Gift Animation",
|
||||
value: _giftEffectsEnabled,
|
||||
onTap: () => _setGiftEffectsEnabled(!_giftEffectsEnabled),
|
||||
),
|
||||
_buildSwitchRow(
|
||||
top: 97.w,
|
||||
title: "Vehicle Animation",
|
||||
value: _entryEffectsEnabled,
|
||||
onTap: () => _setEntryEffectsEnabled(!_entryEffectsEnabled),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchRow({
|
||||
required double top,
|
||||
required String title,
|
||||
required bool value,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Positioned(
|
||||
top: top - 8.w,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 30.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 36.w,
|
||||
top: 8.w,
|
||||
height: 14.w,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: text(
|
||||
title,
|
||||
fontSize: 14,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
lineHeight: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
SCDebounceWidget(
|
||||
onTap: onTap,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 24.w),
|
||||
child: SizedBox(
|
||||
width: 56.w,
|
||||
height: 30.w,
|
||||
child: Center(child: _FigmaAnimationSwitch(value: value)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FigmaAnimationSwitch extends StatelessWidget {
|
||||
const _FigmaAnimationSwitch({required this.value});
|
||||
|
||||
final bool value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = value ? const Color(0xff18F2B1) : const Color(0xff999999);
|
||||
return SizedBox(
|
||||
width: 32.w,
|
||||
height: 14.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7.w),
|
||||
border: Border.all(color: color, width: 1.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: value ? 20.w : 2.w,
|
||||
top: 2.w,
|
||||
child: Container(
|
||||
width: 10.w,
|
||||
height: 10.w,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,89 +1,94 @@
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/shared/tools/sc_banner_utils.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
|
||||
class RoomBannerView extends StatefulWidget {
|
||||
@override
|
||||
_RoomBannerViewState createState() => _RoomBannerViewState();
|
||||
}
|
||||
|
||||
class _RoomBannerViewState extends State<RoomBannerView> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SCAppGeneralManager>(
|
||||
builder: (context, ref, child) {
|
||||
return _banner(ref);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_banner(SCAppGeneralManager ref) {
|
||||
return ref.roomBanners.isEmpty
|
||||
? Container()
|
||||
: SizedBox(
|
||||
width: 46.w,
|
||||
child: Column(
|
||||
children: [
|
||||
CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: 46.w,
|
||||
autoPlay: true,
|
||||
// 启用自动播放
|
||||
enlargeCenterPage: false,
|
||||
// 居中放大当前页面
|
||||
aspectRatio: 1 / 1,
|
||||
// 宽高比
|
||||
enableInfiniteScroll: true,
|
||||
// 启用无限循环
|
||||
autoPlayAnimationDuration: Duration(milliseconds: 800),
|
||||
// 自动播放动画时长
|
||||
viewportFraction: 1,
|
||||
// 视口分数
|
||||
onPageChanged: (index, reason) {
|
||||
_currentIndex = index;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
items:
|
||||
ref.roomBanners.map((item) {
|
||||
return GestureDetector(
|
||||
child: netImage(url: item.smallCover ?? ""),
|
||||
onTap: () {
|
||||
SCBannerUtils.openBanner(item, context);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
ref.roomBanners.asMap().entries.map((entry) {
|
||||
return Container(
|
||||
width: 4.0,
|
||||
height: 4.0,
|
||||
margin: EdgeInsetsDirectional.fromSTEB(2, 3, 2, 0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
_currentIndex == entry.key
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/shared/tools/sc_banner_utils.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
|
||||
class RoomBannerView extends StatefulWidget {
|
||||
const RoomBannerView({super.key});
|
||||
|
||||
@override
|
||||
State<RoomBannerView> createState() => _RoomBannerViewState();
|
||||
}
|
||||
|
||||
class _RoomBannerViewState extends State<RoomBannerView> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SCAppGeneralManager>(
|
||||
builder: (context, ref, child) {
|
||||
return _banner(ref);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_banner(SCAppGeneralManager ref) {
|
||||
return ref.roomBanners.isEmpty
|
||||
? Container()
|
||||
: SizedBox(
|
||||
width: 46.w,
|
||||
child: Column(
|
||||
children: [
|
||||
CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: 46.w,
|
||||
autoPlay: true,
|
||||
// 启用自动播放
|
||||
enlargeCenterPage: false,
|
||||
// 居中放大当前页面
|
||||
aspectRatio: 1 / 1,
|
||||
// 宽高比
|
||||
enableInfiniteScroll: true,
|
||||
// 启用无限循环
|
||||
autoPlayAnimationDuration: Duration(milliseconds: 800),
|
||||
// 自动播放动画时长
|
||||
viewportFraction: 1,
|
||||
// 视口分数
|
||||
onPageChanged: (index, reason) {
|
||||
_currentIndex = index;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
items:
|
||||
ref.roomBanners.map((item) {
|
||||
return GestureDetector(
|
||||
child: netImage(url: item.smallCover ?? ""),
|
||||
onTap: () {
|
||||
SCBannerUtils.openBanner(
|
||||
item,
|
||||
context,
|
||||
openH5AsRoomPopup: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
ref.roomBanners.asMap().entries.map((entry) {
|
||||
return Container(
|
||||
width: 4.0,
|
||||
height: 4.0,
|
||||
margin: EdgeInsetsDirectional.fromSTEB(2, 3, 2, 0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
_currentIndex == entry.key
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
|
||||
import '../../../app/routes/sc_fluro_navigator.dart';
|
||||
@ -238,7 +239,10 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
animationType: SmartAnimationType.fade,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) {
|
||||
return GiftPage();
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_gift_panel',
|
||||
child: GiftPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -254,9 +258,12 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) {
|
||||
return RoomMenuDialog(roomMenuStime1, (eTime) {
|
||||
roomMenuStime1 = eTime;
|
||||
});
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_menu_dialog',
|
||||
child: RoomMenuDialog(roomMenuStime1, (eTime) {
|
||||
roomMenuStime1 = eTime;
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/modules/room_game/views/room_game_list_sheet.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
|
||||
@ -12,11 +15,20 @@ class RoomGameEntryButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SCDebounceWidget(
|
||||
onTap: () {
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomGameBottomSheet(roomContext: context),
|
||||
barrierColor: Colors.black54,
|
||||
final guardLease = SCRoomTopLayerGuard().acquire(
|
||||
reason: 'room_game_list_sheet',
|
||||
);
|
||||
try {
|
||||
unawaited(
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomGameBottomSheet(roomContext: context),
|
||||
barrierColor: Colors.black54,
|
||||
).whenComplete(guardLease.release),
|
||||
);
|
||||
} catch (_) {
|
||||
guardLease.release();
|
||||
}
|
||||
},
|
||||
child: SCSvgaAssetWidget(
|
||||
assetPath: "sc_images/room/sc_icon_room_game_entry_anim.svga",
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/modules/room/detail/room_detail_page.dart';
|
||||
@ -223,7 +224,10 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black54,
|
||||
builder: (_) {
|
||||
return SCEditRoomAnnouncementPage();
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_announcement_dialog',
|
||||
child: SCEditRoomAnnouncementPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_animation_switch_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart';
|
||||
import '../../../app/routes/sc_fluro_navigator.dart';
|
||||
import '../../../modules/index/main_route.dart';
|
||||
@ -34,7 +36,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
static const int _menuMicManagement = 0;
|
||||
static const int _menuClearMessage = 1;
|
||||
static const int _menuPicture = 2;
|
||||
static const int _menuSettings = 3;
|
||||
static const int _menuAnimationEffects = 3;
|
||||
static const int _menuSound = 4;
|
||||
static const int _menuReport = 5;
|
||||
static const int _menuBackground = 6;
|
||||
@ -103,15 +105,13 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
"red_packet/small_packet_icon.png",
|
||||
),
|
||||
);
|
||||
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuSettings,
|
||||
SCAppLocalizations.of(context)!.settings,
|
||||
"sc_icon_room_menu_settins.png",
|
||||
),
|
||||
);
|
||||
}
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuAnimationEffects,
|
||||
SCAppLocalizations.of(context)!.specialEffectsManagement,
|
||||
"sc_icon_room_animation_effects_menu.png",
|
||||
),
|
||||
);
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuSound,
|
||||
@ -236,7 +236,10 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
animationType: SmartAnimationType.fade,
|
||||
debounce: true,
|
||||
builder: (_) {
|
||||
return RoomMicSwitchPage();
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_mic_switch_dialog',
|
||||
child: RoomMicSwitchPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (item.id == _menuClearMessage) {
|
||||
@ -275,12 +278,9 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
} else if (item.id == _menuRedPacket) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
RoomRedPacketSendPanel.show(navigatorKey.currentState!.context);
|
||||
} else if (item.id == _menuSettings) {
|
||||
} else if (item.id == _menuAnimationEffects) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
VoiceRoomRoute.openRoomEdit(
|
||||
navigatorKey.currentState!.context,
|
||||
needRestCurrentRoomInfo: false,
|
||||
);
|
||||
RoomAnimationSwitchDialog.show(navigatorKey.currentState!.context);
|
||||
} else if (item.id == _menuSound) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
Provider.of<RtcProvider>(
|
||||
@ -306,6 +306,24 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
}
|
||||
|
||||
Widget _buildMenuIcon(RoomMenu item) {
|
||||
if (item.id == _menuAnimationEffects) {
|
||||
return Container(
|
||||
width: 45.w,
|
||||
height: 45.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (item.id != _menuMusic && item.id != _menuRedPacket) {
|
||||
return Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/modules/room/online/room_online_page.dart';
|
||||
@ -207,7 +208,10 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return RoomGiftRankPage();
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_gift_rank_dialog',
|
||||
child: RoomGiftRankPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
529
lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart
Normal file
@ -0,0 +1,529 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/payment/google_payment_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
|
||||
class RoomRechargeBottomSheet extends StatefulWidget {
|
||||
const RoomRechargeBottomSheet({super.key});
|
||||
|
||||
static const String dialogTag = 'showRoomRechargeBottomSheet';
|
||||
|
||||
static void show(BuildContext context) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black.withValues(alpha: 0.6),
|
||||
clickMaskDismiss: true,
|
||||
builder:
|
||||
(_) => const SCRoomTopLayerSuppressor(
|
||||
reason: 'room_recharge_bottom_sheet',
|
||||
child: RoomRechargeBottomSheet(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomRechargeBottomSheet> createState() =>
|
||||
_RoomRechargeBottomSheetState();
|
||||
}
|
||||
|
||||
class _RoomRechargeBottomSheetState extends State<RoomRechargeBottomSheet> {
|
||||
static const Color _sheetColor = Color(0xFF072121);
|
||||
static const Color _cardColor = Color(0xFF011414);
|
||||
static const Color _accentColor = Color(0xFF18F2B1);
|
||||
static const String _coinAsset =
|
||||
'sc_images/room/sc_room_recharge_modal_coin.png';
|
||||
|
||||
int _fallbackSelectedIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_initializeRecharge();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _initializeRecharge() {
|
||||
final paymentContext = navigatorKey.currentState?.context ?? context;
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
paymentContext,
|
||||
listen: false,
|
||||
).balance();
|
||||
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
|
||||
final processor = Provider.of<AndroidPaymentProcessor>(
|
||||
paymentContext,
|
||||
listen: false,
|
||||
);
|
||||
if (processor.products.isEmpty) {
|
||||
processor.initializePaymentProcessor(paymentContext);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 306.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _sheetColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)),
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: 7.w,
|
||||
start: 16.w,
|
||||
child: Text(
|
||||
'Your Balance Is Low. Please Add Funds.',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 36.w,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Payment Methods',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: 74.w,
|
||||
start: 16.w,
|
||||
end: 16.w,
|
||||
child: _buildPaymentMethodRow(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: 126.w,
|
||||
start: 16.w,
|
||||
end: 16.w,
|
||||
child: _buildProductOptions(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: 222.w,
|
||||
start: 24.w,
|
||||
end: 24.w,
|
||||
child: _buildConfirmButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodRow() {
|
||||
return Container(
|
||||
height: 40.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: _cardColor,
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'G',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Google Pay',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSelectedRadio(),
|
||||
SizedBox(width: 7.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedRadio() {
|
||||
return Container(
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 10.w,
|
||||
height: 10.w,
|
||||
decoration: const BoxDecoration(
|
||||
color: _accentColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductOptions() {
|
||||
if (!Platform.isAndroid) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: _buildFallbackProductItem(0)),
|
||||
SizedBox(width: 13.w),
|
||||
Expanded(child: _buildFallbackProductItem(1)),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Consumer<AndroidPaymentProcessor>(
|
||||
builder: (context, processor, child) {
|
||||
final products = processor.products.take(2).toList();
|
||||
if (products.isEmpty) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: _buildFallbackProductItem(0)),
|
||||
SizedBox(width: 13.w),
|
||||
Expanded(child: _buildFallbackProductItem(1)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
for (int index = 0; index < products.length; index++) ...[
|
||||
if (index > 0) SizedBox(width: 13.w),
|
||||
Expanded(
|
||||
child: _buildStoreProductItem(
|
||||
processor: processor,
|
||||
productConfig: products[index],
|
||||
index: index,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (products.length == 1) ...[
|
||||
SizedBox(width: 13.w),
|
||||
const Spacer(),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFallbackProductItem(int index) {
|
||||
return _RechargeProductCard(
|
||||
selected: index == _fallbackSelectedIndex,
|
||||
amount: '150,000',
|
||||
originalAmount: '100,000',
|
||||
price: 'USD 4.99',
|
||||
onTap: () => setState(() => _fallbackSelectedIndex = index),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStoreProductItem({
|
||||
required AndroidPaymentProcessor processor,
|
||||
required SelecteProductConfig productConfig,
|
||||
required int index,
|
||||
}) {
|
||||
final product = processor.productMap[productConfig.produc.id];
|
||||
return _RechargeProductCard(
|
||||
selected: productConfig.isSelecte,
|
||||
amount: _formatAmount(_totalCandy(product)),
|
||||
originalAmount: _formatOriginalAmount(product),
|
||||
price: productConfig.produc.price,
|
||||
onTap: () => processor.chooseProductConfig(index),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatOriginalAmount(SCProductConfigRes? product) {
|
||||
if (product == null) {
|
||||
return '';
|
||||
}
|
||||
final obtainCandy = (product.obtainCandy ?? 0).round();
|
||||
final rewardCandy = (product.rewardCandy ?? 0).round();
|
||||
if (obtainCandy <= 0 || rewardCandy <= 0) {
|
||||
return '';
|
||||
}
|
||||
return _formatAmount(obtainCandy);
|
||||
}
|
||||
|
||||
int _totalCandy(SCProductConfigRes? product) {
|
||||
if (product == null) {
|
||||
return 0;
|
||||
}
|
||||
return ((product.obtainCandy ?? 0) + (product.rewardCandy ?? 0)).round();
|
||||
}
|
||||
|
||||
String _formatAmount(int value) {
|
||||
final sign = value < 0 ? '-' : '';
|
||||
final digits = value.abs().toString();
|
||||
final buffer = StringBuffer();
|
||||
for (int index = 0; index < digits.length; index++) {
|
||||
final remaining = digits.length - index;
|
||||
buffer.write(digits[index]);
|
||||
if (remaining > 1 && remaining % 3 == 1) {
|
||||
buffer.write(',');
|
||||
}
|
||||
}
|
||||
return '$sign$buffer';
|
||||
}
|
||||
|
||||
Widget _buildConfirmButton() {
|
||||
return SCDebounceWidget(
|
||||
onTap: _processGooglePurchase,
|
||||
child: Container(
|
||||
height: 44.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _accentColor,
|
||||
borderRadius: BorderRadius.circular(22.w),
|
||||
),
|
||||
child: Text(
|
||||
'Confirm Top-Up',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _processGooglePurchase() async {
|
||||
if (!Platform.isAndroid) {
|
||||
SCTts.show('Google Play payment is only available on Android');
|
||||
return;
|
||||
}
|
||||
|
||||
final pleaseSelectMessage =
|
||||
SCAppLocalizations.of(context)!.pleaseSelectaItem;
|
||||
final paymentContext = navigatorKey.currentState?.context ?? context;
|
||||
final processor = Provider.of<AndroidPaymentProcessor>(
|
||||
paymentContext,
|
||||
listen: false,
|
||||
);
|
||||
if (processor.products.isEmpty) {
|
||||
await processor.initializePaymentProcessor(paymentContext);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final product = _selectedProduct(processor.products);
|
||||
if (product == null) {
|
||||
SCTts.show(pleaseSelectMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await processor.recoverTransactions();
|
||||
SCLoadingManager.show();
|
||||
final success = await processor.iap.buyConsumable(
|
||||
purchaseParam: PurchaseParam(productDetails: product.produc),
|
||||
autoConsume: kReleaseMode,
|
||||
);
|
||||
if (!success) {
|
||||
SCTts.show('Purchase failed, please check your network connection');
|
||||
}
|
||||
} catch (error) {
|
||||
SCTts.show('Purchase failed: $error');
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
SelecteProductConfig? _selectedProduct(List<SelecteProductConfig> products) {
|
||||
for (final product in products) {
|
||||
if (product.isSelecte) {
|
||||
return product;
|
||||
}
|
||||
}
|
||||
return products.isEmpty ? null : products.first;
|
||||
}
|
||||
}
|
||||
|
||||
class _RechargeProductCard extends StatelessWidget {
|
||||
const _RechargeProductCard({
|
||||
required this.selected,
|
||||
required this.amount,
|
||||
required this.originalAmount,
|
||||
required this.price,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool selected;
|
||||
final String amount;
|
||||
final String originalAmount;
|
||||
final String price;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCDebounceWidget(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 72.w,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: _RoomRechargeBottomSheetState._cardColor,
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border:
|
||||
selected
|
||||
? Border.all(
|
||||
color: _RoomRechargeBottomSheetState._accentColor,
|
||||
width: 1.w,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
_RoomRechargeBottomSheetState._coinAsset,
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
amount,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Visibility(
|
||||
visible: originalAmount.isNotEmpty,
|
||||
maintainAnimation: true,
|
||||
maintainSize: true,
|
||||
maintainState: true,
|
||||
child: Text(
|
||||
originalAmount,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationColor: Colors.white.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 20.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(8.w),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
price,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -16,62 +15,47 @@ import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||||
|
||||
class RoomUserCardSetting extends StatefulWidget {
|
||||
String? roomId;
|
||||
RoomUserCardRes? userCardInfo;
|
||||
final String? roomId;
|
||||
final RoomUserCardRes? userCardInfo;
|
||||
|
||||
RoomUserCardSetting({super.key, this.roomId, this.userCardInfo});
|
||||
const RoomUserCardSetting({super.key, this.roomId, this.userCardInfo});
|
||||
|
||||
@override
|
||||
_RoomUserCardSettingState createState() => _RoomUserCardSettingState();
|
||||
State<RoomUserCardSetting> createState() => _RoomUserCardSettingState();
|
||||
}
|
||||
|
||||
class _RoomUserCardSettingState extends State<RoomUserCardSetting> {
|
||||
bool isOnMai = false;
|
||||
bool micMute = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isOnMai =
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).userOnMaiInIndex(widget.userCardInfo?.userProfile?.id ?? "") !=
|
||||
-1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final targetUserId = widget.userCardInfo?.userProfile?.id ?? "";
|
||||
final isTargetRoomManager =
|
||||
widget.userCardInfo?.roomRole == SCRoomRolesType.ADMIN.name ||
|
||||
widget.userCardInfo?.roomRole == SCRoomRolesType.HOMEOWNER.name;
|
||||
final isOnMai = rtcProvider.userOnMaiInIndex(targetUserId) != -1;
|
||||
List<Widget> list = [];
|
||||
list.add(SizedBox(height: 10.w));
|
||||
if (isOnMai) {
|
||||
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
|
||||
if (rtcProvider.isFz()) {
|
||||
///操作的人是房主
|
||||
list.add(
|
||||
_item(SCAppLocalizations.of(context)!.removeTheMic, null, () {
|
||||
Navigator.of(context).pop();
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).killXiaMai(widget.userCardInfo?.userProfile?.id ?? "");
|
||||
rtcProvider.killXiaMai(targetUserId);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
if (widget.userCardInfo?.roomRole != SCRoomRolesType.ADMIN.name &&
|
||||
widget.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name) {
|
||||
if (rtcProvider.isGL() && !isTargetRoomManager) {
|
||||
list.add(
|
||||
_item(SCAppLocalizations.of(context)!.removeTheMic, null, () {
|
||||
Navigator.of(context).pop();
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).killXiaMai(widget.userCardInfo?.userProfile?.id ?? "");
|
||||
rtcProvider.killXiaMai(targetUserId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
|
||||
if (rtcProvider.isFz()) {
|
||||
list.add(
|
||||
_item(SCAppLocalizations.of(context)!.setUpAnIdentity, null, () {
|
||||
Navigator.of(context).pop();
|
||||
@ -79,8 +63,7 @@ class _RoomUserCardSettingState extends State<RoomUserCardSetting> {
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (widget.userCardInfo?.roomRole != SCRoomRolesType.ADMIN.name &&
|
||||
widget.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name) {
|
||||
if (!isTargetRoomManager) {
|
||||
list.add(
|
||||
_item(SCAppLocalizations.of(context)!.kickedOutOfRoom, null, () {
|
||||
Navigator.of(context).pop();
|
||||
@ -116,7 +99,7 @@ class _RoomUserCardSettingState extends State<RoomUserCardSetting> {
|
||||
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
color: Color(0xff09372E).withOpacity(0.5),
|
||||
color: Color(0xff09372E).withValues(alpha: 0.5),
|
||||
child: Column(children: list),
|
||||
),
|
||||
),
|
||||
@ -134,12 +117,12 @@ class _RoomUserCardSettingState extends State<RoomUserCardSetting> {
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration:
|
||||
msg == SCAppLocalizations.of(context)!.cancel
|
||||
? BoxDecoration(
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.all(Radius.circular(32.w)),
|
||||
)
|
||||
: null,
|
||||
msg == SCAppLocalizations.of(context)!.cancel
|
||||
? BoxDecoration(
|
||||
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.all(Radius.circular(32.w)),
|
||||
)
|
||||
: null,
|
||||
width: ScreenUtil().screenWidth * 0.7,
|
||||
padding: EdgeInsets.symmetric(vertical: 10.w),
|
||||
child: Row(
|
||||
@ -156,7 +139,7 @@ class _RoomUserCardSettingState extends State<RoomUserCardSetting> {
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: sp(16),
|
||||
color:Colors.white,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
|
||||
@ -22,6 +22,7 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
@ -1440,7 +1441,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
animationType: SmartAnimationType.fade,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) {
|
||||
return GiftPage(toUser: ref.userCardInfo?.userProfile);
|
||||
return SCRoomTopLayerSuppressor(
|
||||
reason: 'room_user_gift_panel',
|
||||
child: GiftPage(toUser: ref.userCardInfo?.userProfile),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1927,24 +1931,24 @@ class _RoomCpProgressData {
|
||||
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
|
||||
}
|
||||
final levelName = level.levelName?.trim();
|
||||
if (level.level != null && levelName != null && levelName.isNotEmpty) {
|
||||
return "Lv.${level.level} $levelName";
|
||||
if (levelName != null && levelName.isNotEmpty) {
|
||||
return levelName;
|
||||
}
|
||||
if (level.level != null) {
|
||||
return "Lv.${level.level}";
|
||||
}
|
||||
if (levelName != null && levelName.isNotEmpty) {
|
||||
return levelName;
|
||||
}
|
||||
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
|
||||
}
|
||||
|
||||
static String _levelShortText(int? level, {String? fallback}) {
|
||||
final fallbackText = fallback?.trim() ?? "";
|
||||
if (fallbackText.isNotEmpty) {
|
||||
return fallbackText;
|
||||
}
|
||||
if (level != null) {
|
||||
return "Lv. $level";
|
||||
}
|
||||
final fallbackText = fallback?.trim() ?? "";
|
||||
return fallbackText.isEmpty ? "Lv. 1" : fallbackText;
|
||||
return "Lv. 1";
|
||||
}
|
||||
|
||||
static int _levelIndex(String levelText) {
|
||||
|
||||
@ -876,7 +876,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
in_app_purchase_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase_android
|
||||
sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71"
|
||||
|
||||
10
pubspec.yaml
@ -99,10 +99,11 @@ dependencies:
|
||||
path: ./local_packages/flutter_svga-0.0.13-patched
|
||||
#banner
|
||||
carousel_slider: ^5.1.1
|
||||
#谷歌支付
|
||||
in_app_purchase: ^3.2.3
|
||||
|
||||
device_info_plus: ^10.0.0
|
||||
#谷歌支付
|
||||
in_app_purchase: ^3.2.3
|
||||
in_app_purchase_android: ^0.4.0+10
|
||||
|
||||
device_info_plus: ^10.0.0
|
||||
package_info_plus: ^8.0.0
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.1.0
|
||||
@ -167,6 +168,7 @@ flutter:
|
||||
- sc_images/index/
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/first_recharge/
|
||||
- sc_images/room/
|
||||
- sc_images/room/cp_profile/
|
||||
- sc_images/room/cp_rights/
|
||||
|
||||
BIN
sc_images/first_recharge/buy_now_button.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
sc_images/first_recharge/close.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
sc_images/first_recharge/discount_badge.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
sc_images/first_recharge/panel_bottom.png
Normal file
|
After Width: | Height: | Size: 635 KiB |
BIN
sc_images/first_recharge/panel_middle.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
sc_images/first_recharge/panel_top.png
Normal file
|
After Width: | Height: | Size: 773 KiB |
BIN
sc_images/first_recharge/preview_background.png
Normal file
|
After Width: | Height: | Size: 652 KiB |
BIN
sc_images/first_recharge/price_selected_texture.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
sc_images/first_recharge/price_tabs_bg.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
sc_images/first_recharge/reward_avatar_frame_1.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
sc_images/first_recharge/reward_card.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
sc_images/first_recharge/reward_card_coin.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
sc_images/first_recharge/reward_coin_stack.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
sc_images/first_recharge/reward_frame.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
sc_images/first_recharge/reward_gift.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
sc_images/first_recharge/reward_vehicle.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
sc_images/first_recharge/room_gift_entry_card.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
sc_images/first_recharge/title.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
sc_images/room/sc_icon_room_animation_effects_menu.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
sc_images/room/sc_room_recharge_modal_coin.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
71
test/gift_res_cp_relation_type_test.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_system_invit_message_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('SocialChatGiftRes relationType', () {
|
||||
test('reads relationType from the gift payload', () {
|
||||
final gift = SocialChatGiftRes.fromJson({
|
||||
'id': 1,
|
||||
'giftTab': 'CP',
|
||||
'relationType': 'BROTHER',
|
||||
});
|
||||
|
||||
expect(gift.relationType, 'BROTHER');
|
||||
expect(gift.toJson()['relationType'], 'BROTHER');
|
||||
});
|
||||
|
||||
test('reads alias relation fields from nested giftConfig', () {
|
||||
final gift = SocialChatGiftRes.fromJson({
|
||||
'giftConfig': {'id': 2, 'giftTab': 'CP', 'cpRelationType': 'SIS'},
|
||||
});
|
||||
|
||||
expect(gift.relationType, 'SIS');
|
||||
expect(scNormalizeCpRelationType(gift.relationType), 'SISTERS');
|
||||
});
|
||||
});
|
||||
|
||||
group('scNormalizeCpRelationType', () {
|
||||
test('normalizes bro and sis aliases', () {
|
||||
expect(scNormalizeCpRelationType('bro'), 'BROTHER');
|
||||
expect(scNormalizeCpRelationType('sis'), 'SISTERS');
|
||||
});
|
||||
});
|
||||
|
||||
group('scCpRelationTypeForGift', () {
|
||||
test('uses explicit relation type before inferred gift text', () {
|
||||
final gift = SocialChatGiftRes(
|
||||
giftTab: 'CP',
|
||||
relationType: 'bro',
|
||||
giftName: 'sister crown',
|
||||
);
|
||||
|
||||
expect(scCpRelationTypeForGift(gift), 'BROTHER');
|
||||
});
|
||||
|
||||
test('infers sister and cp relation from gift fields', () {
|
||||
final sisterGift = SocialChatGiftRes(giftTab: 'CP', giftCode: 'vip_sis');
|
||||
final cpGift = SocialChatGiftRes(giftTab: 'CP', giftName: 'couple ring');
|
||||
|
||||
expect(scCpRelationTypeForGift(sisterGift), 'SISTERS');
|
||||
expect(scCpRelationTypeForGift(cpGift), 'CP');
|
||||
expect(scIsCpGift(sisterGift), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('SCSystemInvitMessageRes avatar frame', () {
|
||||
test('reads avatar frame fields from invite content', () {
|
||||
final invite = SCSystemInvitMessageRes.fromJson({
|
||||
'userAvatar': 'avatar.png',
|
||||
'userAvatarFrameUrl': 'frame.svga',
|
||||
'userAvatarFrameCover': 'frame.png',
|
||||
});
|
||||
|
||||
expect(invite.userHeaddress, 'frame.svga');
|
||||
expect(invite.userHeaddressCover, 'frame.png');
|
||||
expect(invite.toJson()['userHeaddress'], 'frame.svga');
|
||||
});
|
||||
});
|
||||
}
|
||||
72
test/sc_cp_rights_config_res_test.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart';
|
||||
|
||||
void main() {
|
||||
group('SCCpRightsConfigRes', () {
|
||||
test('parses the updated rights config body', () {
|
||||
final config = SCCpRightsConfigRes.fromJson({
|
||||
'relationType': 'CP',
|
||||
'cpUserId': null,
|
||||
'relationshipStatus': 'NONE',
|
||||
'intimacyValue': 0,
|
||||
'levels': [
|
||||
{
|
||||
'level': 1,
|
||||
'levelName': 'CP 1级',
|
||||
'requiredIntimacyValue': 0,
|
||||
'unlocked': false,
|
||||
'rights': [
|
||||
{
|
||||
'rightKey': 'CP_CABIN_LEVEL_1',
|
||||
'icon': null,
|
||||
'title': 'CP 1级',
|
||||
'desc': 'Unlock when intimacy reaches 0',
|
||||
'unlocked': false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(config.relationType, 'CP');
|
||||
expect(config.cpUserId, isNull);
|
||||
expect(config.relationshipStatus, 'NONE');
|
||||
expect(config.intimacyValue, 0);
|
||||
expect(config.levels, hasLength(1));
|
||||
expect(config.levels.first.levelName, 'CP 1级');
|
||||
expect(config.levels.first.requiredIntimacyValue, 0);
|
||||
expect(config.levels.first.unlocked, false);
|
||||
expect(config.levels.first.rights.first.rightKey, 'CP_CABIN_LEVEL_1');
|
||||
expect(config.levels.first.rights.first.title, 'CP 1级');
|
||||
});
|
||||
|
||||
test('also accepts a full response envelope', () {
|
||||
final config = SCCpRightsConfigRes.fromJson({
|
||||
'status': true,
|
||||
'errorCode': 0,
|
||||
'body': {
|
||||
'relationType': 'SISTERS',
|
||||
'cpUserId': 123,
|
||||
'relationshipStatus': 'ACTIVE',
|
||||
'intimacyValue': 1200,
|
||||
'levels': [
|
||||
{
|
||||
'level': 2,
|
||||
'levelName': 'Sisters 2',
|
||||
'requiredIntimacyValue': 1000,
|
||||
'unlocked': true,
|
||||
'rights': [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.relationType, 'SISTERS');
|
||||
expect(config.cpUserId, '123');
|
||||
expect(config.relationshipStatus, 'ACTIVE');
|
||||
expect(config.intimacyValue, 1200);
|
||||
expect(config.levels.first.level, 2);
|
||||
expect(config.levels.first.unlocked, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
220
需求进度.md
@ -1,3 +1,223 @@
|
||||
# 首充弹窗需求进度(2026-05-21)
|
||||
|
||||
- [x] 2026-05-22 用户反馈:礼物弹窗在 iPhone 17 Pro Max 仍报 `A Stack requires bounded constraints from its parent`;本轮已实际运行 iPhone 17 Pro Max 复现,并依据运行日志修到不再报错。
|
||||
- [x] 2026-05-22 已确认 iPhone 17 Pro Max 模拟器在线,设备 ID:`8D2D87EB-4769-43B7-8DC2-580D46A76B6C`。
|
||||
- [x] 2026-05-22 已在 iPhone 17 Pro Max 启动主 App;控制台先出现 SmartDialog overlay 运行期异常:`RenderAnimatedOpacity ... SmartDialogWidget ... size: MISSING`,说明问题发生在弹窗 overlay 内容尺寸约束层。
|
||||
- [!] 2026-05-22 主 App 运行时有自动签到弹窗/页面跳转干扰,直接从首页进入房间礼物面板不稳定;改为创建只用于验证的礼物弹窗 debug 入口,复用真实 `GiftPage` 与 `SmartDialog.show(tag: showGiftControl)` 路径在 iPhone 17 Pro Max 直接复现。
|
||||
- [x] 2026-05-22 已新增 `lib/debug/gift_page_preview.dart`,用于在 iPhone 17 Pro Max 直接运行真实 `GiftPage` 礼物弹窗,避免主 App 自动弹窗和导航干扰。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/debug/gift_page_preview.dart` 与 `flutter analyze lib/debug/gift_page_preview.dart lib/modules/gift/gift_page.dart`,结果:No issues found。
|
||||
- [!] 2026-05-22 首次运行 debug 礼物弹窗入口成功进入 Flutter,但入口自身有两个干扰:mock `balance()` 在 build 中通知 provider、`AppConfig` 未初始化;先修入口,避免干扰礼物弹窗约束验证。
|
||||
- [x] 2026-05-22 已修正 debug 礼物弹窗入口:补 `AppConfig.initialize()` / `DataPersistence.initialize()`,mock `balance()` 改为 no-op;复跑 format/analyze,结果 No issues found。
|
||||
- [x] 2026-05-22 已在 iPhone 17 Pro Max 礼物弹窗 debug 入口稳定复现用户报错;运行日志明确定位到 `lib/ui_kit/widgets/gift/sc_gift_combo_send_button.dart:80` 的 `Stack`,约束为 `BoxConstraints(w=112.6, 0.0<=h<=Infinity)`。
|
||||
- [x] 2026-05-22 已修复真正报错点:`SCGiftComboSendButton` 原本只有宽度没有高度,导致内部 `Stack` 在 Row/Column 中获得无界高度;现补 `height: 32.w`,不改礼物悬浮板、头像、列表样式。
|
||||
- [x] 2026-05-22 已在 iPhone 17 Pro Max 礼物弹窗 debug 入口热重启验证:等待 5 秒未再出现 `A Stack requires bounded constraints` 或连锁 layout 错误。
|
||||
- [x] 2026-05-22 已保存验证截图:`build/gift_page_verification/ios17promax_gift_panel_no_stack_error.png`。
|
||||
- [x] 2026-05-22 已继续观察 iPhone 17 Pro Max 礼物弹窗预览 10 秒,控制台无新增 `Stack requires bounded constraints`。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/ui_kit/widgets/gift/sc_gift_combo_send_button.dart lib/modules/gift/gift_page.dart lib/debug/gift_page_preview.dart` 与对应 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 已退出 iPhone 17 Pro Max 礼物弹窗预览 `flutter run` 会话,未保留运行进程。
|
||||
- [x] 2026-05-22 用户反馈:礼物弹窗仍报 `A Stack requires bounded constraints`;上轮不应改头像/loading 原有样式,本轮已还原无关样式改动,并按 CP 悬浮板方式把首充悬浮板放到上方。
|
||||
- [x] 2026-05-22 已还原上轮无关样式改动:麦位头像、指定用户头像、礼物 tab loading skeleton 恢复原本写法;本轮不再动这些原有样式。
|
||||
- [x] 2026-05-22 已将 `lib/modules/gift/gift_tab_page.dart` 恢复到仓库原样,避免上轮 loading skeleton 改动残留。
|
||||
- [x] 2026-05-22 已按 CP 悬浮板方式最小调整首充悬浮板:外层边距改为 `10.w`,卡片尺寸改为 `355.w × 46.w`,仍放在礼物主体 `ClipRRect` 上方。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/modules/gift/gift_page.dart` 与 `flutter analyze lib/modules/gift/gift_page.dart`,结果:No issues found。
|
||||
- [x] 2026-05-22 已补充礼物弹窗根宽度约束:`SafeArea` 内新增 `SizedBox(width: ScreenUtil().screenWidth)` 包裹原 `Column`,只解决 SmartDialog 松约束下内部 `Stack/TabBarView` 无界问题,不改头像、列表、loading 等原有样式。
|
||||
- [x] 2026-05-22 已在根宽度约束修复后复跑 `dart format lib/modules/gift/gift_page.dart` 与 `flutter analyze lib/modules/gift/gift_page.dart`,结果:No issues found。
|
||||
- [x] 2026-05-22 根据 iPhone 17 Pro Max 实际日志确认根因是发送按钮高度无界,不是弹窗根宽度;已撤回 `SafeArea` 内新增的根 `SizedBox(width: ScreenUtil().screenWidth)`,继续保持原本弹窗布局。
|
||||
- [x] 2026-05-22 撤回根宽度约束后,已重新执行 format/analyze:`lib/modules/gift/gift_page.dart`、`lib/ui_kit/widgets/gift/sc_gift_combo_send_button.dart`、`lib/debug/gift_page_preview.dart` 均 No issues found。
|
||||
- [x] 2026-05-22 已重新冷启动 iPhone 17 Pro Max 礼物弹窗预览入口,启动完成后观察 10 秒,控制台无 `A Stack requires bounded constraints` 报错。
|
||||
- [x] 2026-05-22 已覆盖保存最终验证截图:`build/gift_page_verification/ios17promax_gift_panel_no_stack_error.png`。
|
||||
- [x] 2026-05-22 已退出最终 iPhone 17 Pro Max 礼物弹窗预览 `flutter run` 会话。
|
||||
- [x] 2026-05-22 新增问题:礼物弹窗弹起时报 `A Stack requires bounded constraints from its parent`,已定位并修复礼物弹窗内无界约束的 `Stack`。
|
||||
- [x] 2026-05-22 已定位礼物弹窗内高风险 `Stack`:麦位头像 `_maiHead()` 与指定用户头像叠选中角标时,处在 `Row/SingleChildScrollView` 的松约束链路中,需要补固定尺寸。
|
||||
- [x] 2026-05-22 已修复礼物弹窗头像叠层约束:指定用户头像 `Stack` 包裹 `30.w × 30.w`,麦位头像外层 `Stack` 包裹 `32.w × 32.w`、内层头像叠层包裹 `30.w × 30.w`。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/modules/gift/gift_page.dart` 与 `flutter analyze lib/modules/gift/gift_page.dart`,结果:No issues found。
|
||||
- [x] 2026-05-22 已修复礼物 tab loading skeleton:`_buildGiftPageSkeleton()` 改为显式接收高度,并用有限宽高 `SizedBox` 包裹 `Stack`;空数据 loading 使用 `kGiftTabPageHeight.w`,分页 loading 使用 `_kGiftGridHeight.w`。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/modules/gift/gift_page.dart lib/modules/gift/gift_tab_page.dart` 与 `flutter analyze lib/modules/gift/gift_page.dart lib/modules/gift/gift_tab_page.dart`,结果:No issues found。
|
||||
- [x] 2026-05-22 已完成本次报错修复收尾核对:本轮实际修复文件为 `lib/modules/gift/gift_page.dart`、`lib/modules/gift/gift_tab_page.dart` 与本进度文档;未改动其它业务逻辑。
|
||||
- [x] 2026-05-22 新增需求:用户未首充时,房间礼物弹窗上方新增首充悬浮板;CP 礼物 tab 保持特殊 CP 悬浮板,其它礼物 tab 展示 Figma 节点 `191:388` 样式;点击悬浮板打开首充弹窗。
|
||||
- [x] 2026-05-22 已开始定位礼物弹窗:`lib/modules/gift/gift_page.dart` 包含旧首充入口逻辑、CP 礼物 tab 判断与 `_buildCpGiftCloseFriendEntry()` 特殊 CP 悬浮板。
|
||||
- [x] 2026-05-22 已确认现有 CP 悬浮板实现:`_buildCpGiftCloseFriendEntry()` 使用 `sc_images/room/cp_profile/sc_cp_profile_gift_entry_card.png`,尺寸 `355.w × 46.w`,位于礼物弹窗主体上方。
|
||||
- [x] 2026-05-22 已通过本机 Figma Desktop MCP 读取节点 `191:388` 的结构化设计信息和截图。
|
||||
- [!] 2026-05-22 本机 Python 未安装 `requests`,改用 `curl` 直接访问 `http://127.0.0.1:3845/mcp`。
|
||||
- [x] 2026-05-22 已完成 Figma Desktop MCP 初始化握手,服务返回 `Figma Dev Mode MCP Server`。
|
||||
- [x] 2026-05-22 已读取 Figma 节点 `191:388` metadata:节点名 `Group 2090056029`,尺寸 `343 × 50`,主体由多层图片/遮罩组成。
|
||||
- [x] 2026-05-22 已读取 Figma 节点 `191:388` design context:样式依赖多张 PNG/SVG 资源和 mask;为保证 Flutter 还原度,本轮采用节点截图作为悬浮板资源。
|
||||
- [x] 2026-05-22 已通过 Figma Desktop MCP 获取节点 `191:388` screenshot。
|
||||
- [x] 2026-05-22 已将 Figma 节点 `191:388` 截图落地为项目资源:`sc_images/first_recharge/room_gift_entry_card.png`,尺寸 `343 × 50`。
|
||||
- [x] 2026-05-22 已完成礼物弹窗改造:移除旧顶部小首充 tag;在非 CP 礼物 tab、用户未首充、非审核状态下展示新的横向首充悬浮板。
|
||||
- [x] 2026-05-22 已更新 `GiftPage`:新增 `sc_images/first_recharge/room_gift_entry_card.png` 资源引用,普通礼物 tab 按 `firstRecharge=true` 显示横向悬浮板。
|
||||
- [x] 2026-05-22 已移除礼物弹窗顶部旧的小首充 tag,避免与新横向悬浮板重复展示。
|
||||
- [x] 2026-05-22 已新增 `_buildFirstRechargeGiftEntry()`,尺寸 `343.w × 50.w`,水平边距 `16.w`,位置与 CP 悬浮板同为礼物主体上方。
|
||||
- [x] 2026-05-22 已新增 `_openFirstRechargeDialog()`:点击首充悬浮板先关闭 `showGiftControl` 礼物面板,再延迟打开现有首充弹窗。
|
||||
- [x] 2026-05-22 已清理旧首充 tag 删除后不再使用的 `CountdownTimer` import。
|
||||
- [!] 2026-05-22 首次分析发现 `_openFirstRechargeDialog()` 延迟后使用 `BuildContext` 触发 lint,已改为延迟后重新获取 `navigatorKey.currentState` 并检查 `mounted`。
|
||||
- [x] 2026-05-22 已复跑 `dart format lib/modules/gift/gift_page.dart` 与 `flutter analyze lib/modules/gift/gift_page.dart`,结果:No issues found。
|
||||
- [x] 2026-05-22 已目检新增资源 `room_gift_entry_card.png`:显示 `Newcomer Special Offer`、金币和礼盒,符合 Figma 节点 `191:388` 悬浮板视觉。
|
||||
- [x] 2026-05-22 已完成收尾核对:当前工作区存在多项此前/并行任务改动,本轮新增悬浮板核心变更为 `GiftPage`、新增首充悬浮板图片与本进度文档;未回退无关改动。
|
||||
- [x] 2026-05-22 用户反馈 3:第二档金额 tab 右侧仍离选择器边框有间距;需要继续微调第二档选中态贴边,并说明当前首充弹窗逻辑。
|
||||
- [x] 2026-05-22 已定位第二档右侧留白根因:选择器总宽 `240`,第二档选中态仍按 `left=120,width=110` 渲染,因此右侧天然剩 `10px`。
|
||||
- [x] 2026-05-22 已将第二档选中态改为 `left=120,width=120`,右侧直接铺到选择器边框;第二档文字改为 `left=125,width=110`,保持在右半区居中。
|
||||
- [x] 2026-05-22 已针对第二档贴边修正执行 `dart format` 和定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 已重新运行 iPhone 17 Pro 预览,点击第二档并截图:`build/first_recharge_verification/ios17pro_first_recharge_tab2_flush_v1.png`;另生成 375x812 图 `ios17pro_first_recharge_tab2_flush_v1_375w.png`。
|
||||
- [x] 2026-05-22 已目检新版截图:第二档橙色选中态右侧已顶到选择器右边框,原先 10px 浅底留白已消失。
|
||||
- [x] 2026-05-22 已退出本轮 `flutter run` session,未保留运行命令占用。
|
||||
- [x] 2026-05-22 用户反馈 2:第二个金额 tab 选中态仍未对齐;首充奖励接口文档已提供,需要对接接口;点击 `Buy Now` 需要直接拉起对应 Google 支付。
|
||||
- [x] 2026-05-22 已读取 `/Users/nigger/Desktop/首充奖励-Flutter接口对接文档.md`,确认首充奖励首页接口为 `GET /go/app/first-recharge-reward/home`,金额档位和奖励列表由 `levelConfigs/rewardItems` 返回。
|
||||
- [x] 2026-05-22 已定位现有入口:主页首充浮层和礼物页均调用 `SCDialogUtils.showFirstRechargeDialog`;后续将该入口切到新版首充弹窗并接接口。
|
||||
- [x] 2026-05-22 已检查现有网络层和支付链路:`NetworkClient` 自动注入鉴权/语言/来源请求头并解析 `body`;`AndroidPaymentProcessor` 已由根 `Provider` 注册,现有 Google 内购商品配置来自 `/order/product-config`。
|
||||
- [x] 2026-05-22 正在新增首充奖励接口模型、仓库方法、弹窗数据转换和 `Buy Now` 直拉 Google 支付能力。
|
||||
- [x] 2026-05-22 已新增首充奖励接口响应模型 `SCFirstRechargeRewardHomeRes/LevelConfig/RewardItem`,兼容金额、折扣、总价值、非金币天数角标和金币奖励识别。
|
||||
- [x] 2026-05-22 已在 `SocialChatConfigRepository` / `SCConfigRepositoryImp` 增加 `firstRechargeRewardHome()`,请求 `GET /go/app/first-recharge-reward/home`。
|
||||
- [x] 2026-05-22 已将 `SCDialogUtils.showFirstRechargeDialog` 切换到新版 `FirstRechargeDialog.showFromRemote`,旧入口调用保持不变,主页/礼物页点击会走新接口弹窗。
|
||||
- [x] 2026-05-22 已为 `FirstRechargeDialogData` 增加接口数据转换:金额 tab、奖励卡、金币/非金币样式、折扣角标、总价值文案均来自接口字段或兼容扩展字段。
|
||||
- [x] 2026-05-22 已为 `AndroidPaymentProcessor` 增加 `processFirstRechargePurchase`:优先按接口返回的 `productPackage` 匹配 Google 商品,否则按 `rechargeAmountCents` 匹配价格并直接拉起 Google 支付。
|
||||
- [x] 2026-05-22 已调整第二个金额 tab 的选中贴图裁切:第二档使用右侧负偏移裁切镜像纹理,避免切换到第二档时贴图与边框错位。
|
||||
- [!] 2026-05-22 首次格式化误将 `需求进度.md` 传给 `dart format`,Dart 文件已格式化,Markdown 格式化报错不影响代码;下一步重新仅格式化 Dart 文件。
|
||||
- [!] 2026-05-22 首次定向分析发现支付文件存在若干历史 lint;本轮已清理未使用调试循环、空 `catch`、异步后使用 `BuildContext` 的写法,并将已直接导入的 `in_app_purchase_android` 补为显式依赖。
|
||||
- [x] 2026-05-22 已执行 `flutter pub get`,`in_app_purchase_android` 从传递依赖变为显式直接依赖。
|
||||
- [!] 2026-05-22 第二次格式化误将 `pubspec.yaml` 传给 `dart format`,`pub get` 与 Dart 文件格式化已成功;下一步仅对 Dart 文件执行格式化/分析。
|
||||
- [x] 2026-05-22 已重新仅格式化 Dart 文件,并执行定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 准备在 iPhone 17 Pro 预览中切换到第二个金额 tab 截图,验证本轮对齐调整。
|
||||
- [x] 2026-05-22 已启动 `lib/debug/first_recharge_dialog_preview.dart` 到 iPhone 17 Pro 模拟器,应用进入 Flutter debug 运行状态。
|
||||
- [!] 2026-05-22 `simctl screenshot` 使用相对路径仍会被 iOS 工具误判目录不存在,改用绝对路径重新截图。
|
||||
- [x] 2026-05-22 已保存 iPhone 17 Pro 预览截图:`build/first_recharge_verification/ios17pro_first_recharge_api_tab1.png`。
|
||||
- [x] 2026-05-22 已在 Simulator 中点击 `$2.99` 第二档并保存截图:`build/first_recharge_verification/ios17pro_first_recharge_api_tab2.png`;另生成 375x812 对比图 `ios17pro_first_recharge_api_tab2_375w.png`。
|
||||
- [x] 2026-05-22 目检第二档金额区域:`$2.99` 文案居中,橙色选中态左侧与中间分割线对齐、右侧裁切贴合外边框,较上一版错位已修正。
|
||||
- [x] 2026-05-22 已退出本轮 `flutter run` session,未保留运行命令占用。
|
||||
- [x] 2026-05-22 已补充远程弹窗加载流程:首充接口返回并完成数据判定后先关闭 loading,再展示首充弹窗,避免 loading 遮住弹窗。
|
||||
- [x] 2026-05-22 已在加载流程修正后复跑 `dart format` 与定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 已处理接口档位数量边界:当前 Figma 弹窗为两个金额 tab,接口映射时仅取前两个可用档位,避免第三档数据与两个 tab UI 状态不一致。
|
||||
- [x] 2026-05-22 已在档位数量边界修正后再次复跑 `dart format` 与定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 用户反馈:顶部金额文字未居中,选中态没有和选择器边框对齐;本轮开始针对金额选择器做细微调整。
|
||||
- [x] 2026-05-22 已定位当前实现:金额区域在 `_PriceSelector` 中,选中态使用 `ClipRRect` 圆角裁剪,和 Figma 异形遮罩/边框咬合不够贴合。
|
||||
- [x] 2026-05-22 已将金额选中态改为 Figma SVG 同款自定义裁剪路径:选中第一档时右侧平直对齐中间边界,选中第二档时镜像处理;同时金额文字按 Figma 的 `top=7 height=16` 放回视觉中心。
|
||||
- [!] 2026-05-22 首次复跑分析发现 `Matrix4.scale` 为新版 Flutter 废弃 API,已改为 `Matrix4.diagonal3Values`。
|
||||
- [x] 2026-05-22 已复跑 `dart format` 和定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 2026-05-22 已在 iPhone 17 Pro 重新运行预览并截图:`build/first_recharge_verification/ios17pro_first_recharge_price_tuned.png`;另生成 375x812 对比图 `ios17pro_first_recharge_price_tuned_375w.png`。
|
||||
- [x] 2026-05-22 复查金额区域:`$0.99` / `$2.99` 已回到各自 tab 中心,橙色选中态右侧平直边已和中间分割边对齐,整体比上一版贴近 Figma。
|
||||
- [x] 2026-05-22 已退出本轮 `flutter run` session,未保留运行命令占用。
|
||||
- [x] 已确认本轮目标:按 Figma `<首充弹窗>` 先实现 UI,数据后续由接口决定;金额选项切换后下方奖励、折扣文案随数据变化。
|
||||
- [x] 已确认工作目录:`/Users/nigger/Documents/GitHub/chatapp3-flutter`。
|
||||
- [x] 已开始按“每一步同步更新本文件”的要求记录本轮进度,历史进度不覆盖。
|
||||
- [x] 已初步扫描项目文件,确认存在每日签到弹窗 `lib/ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart`,本轮将优先复用其弹窗写法和项目 UI 规范。
|
||||
- [!] 内置 Figma app 读取节点 `144:1183` 返回重新认证提示;按本轮要求不走 Remote/OAuth,改为直接访问本机 Figma Desktop MCP:`http://127.0.0.1:3845/mcp`。
|
||||
- [x] 已确认本机 Figma Desktop MCP 服务在线;未初始化 session 的请求返回 `Invalid sessionId`,下一步执行 MCP 初始化握手。
|
||||
- [x] 已完成 Figma Desktop MCP 初始化握手,服务返回 `Figma Dev Mode MCP Server`。
|
||||
- [x] 已通过 Desktop MCP 列出可用工具:`get_metadata`、`get_design_context`、`get_screenshot`、`get_variable_defs` 等。
|
||||
- [x] 已通过 Desktop MCP 读取节点 `144:1183` metadata:该节点本身即目标 frame `首充弹窗`,尺寸 `375x812`;弹窗主体 `340x528`,位置 `x=18 y=153`。
|
||||
- [x] 已通过 Desktop MCP 读取节点 `144:1183` design context;关键图层包括背景遮罩、弹窗三段背景、顶部标题图、金额切换条、6 个奖励卡片、`Buy Now` 按钮和折扣角标。
|
||||
- [x] 已通过 Desktop MCP 读取节点 `144:1183` screenshot,并保存为 `build/first_recharge_verification/figma_reference.png` 作为视觉基准。
|
||||
- [ ] 正在检查 Flutter 项目主题、组件、资源注册方式和每日签到弹窗实现细节。
|
||||
- [x] 已检查 `pubspec.yaml`:资源按目录注册,现有 `daily_sign_in/`、`register_reward/` 均通过 `sc_images/...` 目录管理。
|
||||
- [x] 已检查每日签到弹窗:采用 `SmartDialog.show`、`ScreenUtil`、`Stack + Image.asset` 叠图层,并在弹窗内部定义数据模型与 mock 数据。
|
||||
- [x] 已确认奖励图标策略可复用:金币样式复用 `sc_images/register_reward/yumi_coin.png`;非金币样式支持接口图片 URL 或本地占位。
|
||||
- [x] 已检查当前 git 状态:除本轮 `需求进度.md` 外,已有无关改动 `lib/shared/data_sources/sources/local/floating_screen_manager.dart`,本轮不会回退。
|
||||
- [x] 已下载 Figma 首充弹窗素材到 `sc_images/first_recharge/`,包含弹窗三段背景、标题图、金额切换条、奖励卡、按钮、关闭按钮和折扣角标。
|
||||
- [x] 已按展示尺寸将关闭按钮、标题、按钮、折扣角标和奖励小图压缩到约 3x 资源尺寸,避免 Figma 小图超大分辨率直接进包。
|
||||
- [x] 已在 `pubspec.yaml` 注册 `sc_images/first_recharge/` 资源目录。
|
||||
- [x] 已新增 `lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart`:包含首充套餐数据模型、金额切换、奖励列表、金币/非金币两种奖励样式、折扣角标和 Buy Now 点击回调。
|
||||
- [x] 已新增 `lib/debug/first_recharge_dialog_preview.dart`:使用 Figma 背景图和 `SmartDialog` 遮罩展示首充弹窗,供 iOS 模拟器截图验证。
|
||||
- [x] 已执行 `dart format lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart lib/debug/first_recharge_dialog_preview.dart`,格式化完成。
|
||||
- [!] 首次定向 `flutter analyze` 发现 `previewBackground` 私有常量未使用,已删除。
|
||||
- [x] 已复跑 `flutter analyze lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart lib/debug/first_recharge_dialog_preview.dart`,结果:No issues found。
|
||||
- [x] 已确认 iPhone 17 Pro 模拟器已启动,设备 ID:`49E28192-AC58-4EE3-BF9E-0363C4A47536`。
|
||||
- [ ] 正在运行 `lib/debug/first_recharge_dialog_preview.dart` 到 iPhone 17 Pro 模拟器。
|
||||
- [!] iOS 构建遇到 Xcode concurrent builds 重试;已确认冲突来自另一个正在编译 `lib/main.dart` 的 iPhone 17 Pro Max 构建,本轮暂不主动终止无关运行进程。
|
||||
- [!] 已截取第一张 iPhone 17 Pro 图,但前台显示为既有主 App 每日签到弹窗,不是本轮 preview;判断为同一模拟器已有 Runner/debug session 抢占前台,需终止 Runner 后干净重跑。
|
||||
- [x] 已退出本轮 `flutter run` session,并确认 iPhone 17 Pro 上当前没有正在运行的 `com.org.yumiparty`。
|
||||
- [ ] 正在干净重跑首充弹窗 debug 预览入口。
|
||||
- [x] 已重新截图 `build/first_recharge_verification/ios17pro_first_recharge_v2.png`,首充弹窗可见,主体布局接近 Figma。
|
||||
- [!] 视觉复查发现 debug 预览背景自带状态栏,同时模拟器真实状态栏又叠加一层;已在 debug 预览入口隐藏系统 UI,准备重新截图。
|
||||
- [x] 已重新截图 `build/first_recharge_verification/ios17pro_first_recharge_v3.png`,状态栏重复问题已解决;另生成 375x812 缩放对比图 `ios17pro_first_recharge_v3_375w.png`。
|
||||
- [!] 缩放对比发现金额切换条价格文字整体右偏,`$0.99` 与 `$2.99` 坐标不符合 Figma;已按 price tab 内部相对坐标修正。
|
||||
- [x] 已在价格文字修正后复跑 `dart format` 与定向 `flutter analyze`,结果:No issues found。
|
||||
- [x] 已热重启 iPhone 17 Pro 预览并截取最终图:`build/first_recharge_verification/ios17pro_first_recharge_final.png`;另生成 375x812 对比图 `ios17pro_first_recharge_final_375w.png`。
|
||||
- [x] 最终视觉复查:弹窗主体位置、三段背景、标题、倒计时、金额 tab、6 个奖励卡、总价值文案、Buy Now 按钮和折扣角标已贴近 Figma;金额切换状态的数据结构已支持选择不同金额显示不同奖励。
|
||||
- [!] 剩余客观差异:iPhone 17 Pro 模拟器会显示 Dynamic Island 黑区,而 Figma 截图尺寸为 `375x812` 的静态状态栏背景;这属于设备壳层差异,不影响弹窗主体。
|
||||
- [x] 已退出首充弹窗 `flutter run` session,未保留本轮运行命令占用。
|
||||
- [x] 已完成最终 git 状态检查:本轮新增/修改集中在首充弹窗组件、debug 入口、`sc_images/first_recharge/`、`pubspec.yaml` 和本进度文件;工作区仍存在其他房间/动效相关改动,判断为本轮之外的既有改动,未回退。
|
||||
- [x] 已复查资源体积:将仅用于 debug 预览的背景图压到 `375x812`,并删除未使用的 `price_selected_mask.svg`。
|
||||
- [x] 已在资源整理后最终复跑定向 `flutter analyze`,结果:No issues found。
|
||||
- [ ] 待检查 Flutter 项目主题、组件、资源注册方式和每日签到弹窗实现细节。
|
||||
- [ ] 待实现首充弹窗 UI、预览入口与占位数据模型。
|
||||
- [ ] 待启动 iOS 模拟器截图并与 Figma 截图对比迭代。
|
||||
|
||||
---
|
||||
|
||||
# 语音房内动画开关需求进度(2026-05-21)
|
||||
|
||||
- [x] 2026-05-22 收到第三次反馈:菜单特效图标需改用 `/Users/nigger/Desktop/app素材/figma-20260522-120235.png`,并且外层要和其它菜单图标一样用 `Container` 包裹。
|
||||
- [x] 2026-05-22 已检查本地图片:`figma-20260522-120235.png` 为 45×45 PNG,可直接作为房间菜单图标资源。
|
||||
- [x] 2026-05-22 已用本地图片覆盖项目资源 `sc_images/room/sc_icon_room_animation_effects_menu.png`。
|
||||
- [x] 2026-05-22 已修改菜单特效入口:外层改为和音乐/红包一致的 45×45 圆形 `Container`,内部居中渲染图标。
|
||||
- [x] 2026-05-22 已执行 `dart format lib/ui_kit/widgets/room/room_menu_dialog.dart`。
|
||||
- [x] 2026-05-22 已执行 `flutter analyze --no-fatal-warnings --no-fatal-infos lib/ui_kit/widgets/room/room_menu_dialog.dart`,结果 `No issues found`。
|
||||
- [x] 2026-05-22 已检查最终状态:图标资源已为 45×45 PNG,菜单特效入口代码已引用该资源并包裹 45×45 圆形 `Container`;工作区仍有大量既有/并行改动,本轮仅处理菜单图标和进度记录。
|
||||
|
||||
- [x] 2026-05-22 收到二次反馈:当前功能效果正确,但弹窗 UI 样式不对;需要重新尝试连接 Figma Lalu 节点 `171:264`,并将菜单栏 UI 图标换成节点 `171:261` 对应样式。
|
||||
- [x] 2026-05-22 已重新连接本机 Figma Desktop MCP:`http://127.0.0.1:3845/mcp`,session 为 `177330e2-d0e2-4cdb-9429-febe997ba070`,服务为 `Figma Dev Mode MCP Server`。
|
||||
- [x] 2026-05-22 已通过 Desktop MCP 读取节点 `171:264` metadata / design context:目标为 375×812 整屏,底部弹层从 y=661 起,高 151,背景 `#072121`,顶部圆角 12,标题 `Animation` 18px,中间两行文字 14px,开关尺寸 32×14。
|
||||
- [x] 2026-05-22 已通过 Desktop MCP 读取节点 `171:261` metadata / design context:菜单图标为 26×26 vector,素材地址由本机 Figma assets 服务返回。
|
||||
- [x] 2026-05-22 已获取 `171:264` / `171:261` 截图数据:Figma screenshot 以 MCP 内联 PNG 返回,后续从本地 SSE 文件落盘用于视觉对照。
|
||||
- [x] 2026-05-22 已检查当前实现:旧弹窗为居中 330 宽卡片,和 Figma 底部 375×151 半屏面板不一致;菜单仍使用旧 `sc_icon_room_special_effects.png` 资源,不是节点 `171:261` 的 26×26 vector。
|
||||
- [x] 2026-05-22 已将 Figma 截图落盘:`build/voice_room_animation_switch_verification/figma_171_264_animation_switch.png`、`build/voice_room_animation_switch_verification/figma_171_261_menu_icon.png`。
|
||||
- [x] 2026-05-22 已将节点 `171:261` 菜单图标导出到项目资源:`sc_images/room/sc_icon_room_animation_effects_menu.png`,尺寸 26×26。
|
||||
- [x] 2026-05-22 已目检 Figma 截图:弹窗应为底部全宽面板,不是居中卡片;标题为 `Animation` 且无左侧图标/关闭按钮;行文本分别为 `Gift Animation`、`Vehicle Animation`;开关为 32×14 小胶囊。
|
||||
- [x] 2026-05-22 已确认 Figma 开关素材:开启态为 `#18F2B1` 描边与右侧 10px 圆点,关闭态为 `#999999` 描边与左侧 10px 圆点;当前用 Flutter 自绘,避免 SVG CSS 变量兼容问题。
|
||||
- [x] 2026-05-22 已完成编码:弹窗改为底部 151 高面板、自绘 32×14 开关;菜单图标改为 `sc_icon_room_animation_effects_menu.png` 并按 26×26 渲染。
|
||||
- [x] 2026-05-22 已执行 `dart format` 格式化 `room_animation_switch_dialog.dart` 与 `room_menu_dialog.dart`。
|
||||
- [x] 2026-05-22 已执行 `flutter analyze --no-fatal-warnings --no-fatal-infos lib/ui_kit/widgets/room/room_animation_switch_dialog.dart lib/ui_kit/widgets/room/room_menu_dialog.dart lib/debug/room_animation_switch_dialog_preview.dart`,结果 `No issues found`。
|
||||
- [x] 2026-05-22 已调整 debug preview:`RoomAnimationSwitchDialog` 在预览中同样贴底展示,不再居中。
|
||||
- [x] 2026-05-22 已确认 iPhone 17 Pro Max 模拟器在线,设备 ID `8D2D87EB-4769-43B7-8DC2-580D46A76B6C`。
|
||||
- [x] 2026-05-22 已复跑 `flutter analyze --no-fatal-warnings --no-fatal-infos`,结果 `No issues found`。
|
||||
- [x] 2026-05-22 已重新运行 iPhone 17 Pro Max 预览,Flutter run 已进入热重载状态。
|
||||
- [!] 2026-05-22 首次 `simctl screenshot` 使用相对路径时被 iOS 工具误判目录不存在,已改用绝对路径重新截图。
|
||||
- [x] 2026-05-22 已保存新版弹窗预览截图:`build/voice_room_animation_switch_verification/ios17promax_animation_switch_dialog_figma_style_v1.png`。
|
||||
- [!] 2026-05-22 已目检新版预览截图:底部位置/高度/颜色/标题结构与 Figma 对齐,但 debug preview 因全局默认值让第二个开关显示开启,Figma 基准为关闭;准备仅调整 debug preview 初始状态后复截。
|
||||
- [x] 2026-05-22 已调整 debug preview 初始状态:礼物开关开启、进场开关关闭,仅用于视觉对比;生产弹窗仍读取用户真实偏好。
|
||||
- [x] 2026-05-22 已再次分析 debug preview 与弹窗文件,结果 `No issues found`。
|
||||
- [!] 2026-05-22 尝试对当前 `flutter run` 会话发送热重启时发现 stdin 已关闭,改为重新拉起预览目标后复截。
|
||||
- [x] 2026-05-22 已清理旧的 iPhone 17 Pro Max 预览 `flutter run` / Runner 进程。
|
||||
- [x] 2026-05-22 已重新启动预览并进入热重载状态。
|
||||
- [x] 2026-05-22 已保存第二版对比截图:`build/voice_room_animation_switch_verification/ios17promax_animation_switch_dialog_figma_style_v2.png`。
|
||||
- [x] 2026-05-22 已目检第二版截图:底部面板位置/高度/圆角/背景色、标题与两行开关状态已贴近 Figma 171:264;截图保存为 `ios17promax_animation_switch_dialog_figma_style_v2.png`。
|
||||
- [!] 2026-05-22 停止预览时工具侧 stdin 再次关闭,已改用进程清理方式结束本次 `flutter run` / Runner。
|
||||
- [x] 2026-05-22 已确认 iPhone 17 Pro Max 预览相关 `flutter run` / Runner 进程已退出。
|
||||
- [x] 2026-05-22 已检查最终工作区状态:本轮新增 `sc_images/room/sc_icon_room_animation_effects_menu.png`,并更新动画开关弹窗、房间菜单与 debug preview;工作区仍存在首充弹窗、`floating_screen_manager.dart`、`pubspec.yaml` 等既有/并行改动,本轮不回退。
|
||||
|
||||
- [x] 已确认工作目录为 `/Users/nigger/Documents/GitHub/chatapp3-flutter`,并开始按“每一步同步更新本文件”的要求记录本轮进度。
|
||||
- [x] 已完成初步仓库巡检:项目为 Flutter 工程,资源通过 `pubspec.yaml` 注册,当前存在既有未提交改动 `lib/shared/data_sources/sources/local/floating_screen_manager.dart`,本轮不会回退无关改动。
|
||||
- [x] 已初读现有进度文档、`pubspec.yaml` 与房间/礼物/特效关键字,确认已有全局礼物特效配置、房间动效调度与语音房菜单相关实现需要继续定位。
|
||||
- [!] 已尝试使用内置 Figma app 读取节点 `171:264`,但该连接要求重新认证;按本轮要求不走 Remote/OAuth,改为直接访问本机 Figma Desktop MCP:`http://127.0.0.1:3845/mcp`。
|
||||
- [x] 已成功与 Figma Desktop MCP 完成初始化握手,服务返回 `Figma Dev Mode MCP Server`,后续节点读取将使用该本机 session。
|
||||
- [!] 已通过 Desktop MCP 调用 `get_metadata(nodeId: 171:264)`,当前 Figma active tab 返回未找到该节点;继续读取当前文件顶层结构以定位目标 frame。
|
||||
- [x] 已读取当前 Figma Desktop 文件顶层页面:`0:1 lalu`、`3:15367 yumi`,确认 Desktop MCP 当前连接的是目标 Lalu 文件上下文。
|
||||
- [x] 已扫描两页 metadata:`首充弹窗` 在 `yumi` 页定位到 frame `144:1183`;本轮“Entry Effect / Mic Animation”开关稿在 `yumi` 页房间弹窗区域出现,继续锁定具体 frame。
|
||||
- [x] 已通过 Desktop MCP 获取 `144:1183` 的 design context 与 screenshot,并保存视觉基准到 `build/voice_room_animation_switch_verification/figma_144_1183_first_recharge.png`。
|
||||
- [x] 已通过 Desktop MCP 获取当前可定位的房间设置页 `7:6301` 的 design context 与 screenshot,并保存到 `build/voice_room_animation_switch_verification/figma_7_6301_room_setup.png`;原链接节点 `171:259/171:264` 在当前 Desktop 文件中仍不可直接访问。
|
||||
- [x] 已完成第一轮代码定位:语音房菜单入口在 `lib/ui_kit/widgets/room/room_menu_dialog.dart`;房间可视特效总可见性来自 `RtcProvider.shouldShowRoomVisualEffects`;礼物特效开关已有 `SCGlobalConfig.isGiftSpecialEffects`,进场/座驾开关已有 `SCGlobalConfig.isEntryVehicleAnimation` 但当前播放链路仍需补齐独立判断。
|
||||
- [x] 已确认可复用素材:菜单新图标使用现有 `sc_images/room/sc_icon_room_special_effects.png`,开关控件复用 `sc_images/general/sc_icon_switch_on.png` / `sc_icon_switch_off.png`;无需新增资源目录。
|
||||
- [x] 已完成首轮编码:新增 `RoomAnimationSwitchDialog`,菜单中移除原 Settings 入口并替换为 `sc_icon_room_special_effects.png` 特效管理入口;弹窗内提供“礼物特效 / 进场特效”两个本地持久化开关。
|
||||
- [x] 已补齐播放链路判断:关闭礼物开关时同步关闭普通礼物与幸运礼物特效偏好;关闭进场开关时阻断高成本进场播放、低性能进场兜底与房间进场横幅队列。
|
||||
- [x] 已执行 `dart format`,格式化目标文件通过。
|
||||
- [!] 已执行定向 `flutter analyze`,结果无 error,但 `rtc_manager.dart` / `rtm_manager.dart` 既有 warning/info 让命令按 1 退出;继续使用 `--no-fatal-warnings --no-fatal-infos` 复核 error 口径。
|
||||
- [x] 已执行 `flutter analyze --no-fatal-warnings --no-fatal-infos ...`,退出码 0,确认本轮改动无 error;剩余 27 条 warning/info 均来自既有 `rtc_manager.dart` / `rtm_manager.dart` 风格问题。
|
||||
- [x] 已检查 iOS 模拟器设备:`iPhone 17 Pro Max` 已在线,设备 ID `8D2D87EB-4769-43B7-8DC2-580D46A76B6C`。
|
||||
- [x] 已启动 iPhone 17 Pro Max 运行当前 App;Flutter run 已进入可热重载状态,控制台确认 `lowPerformance=false`、`allowsHighCost=true`。
|
||||
- [x] 已通过模拟器 UI 进入语音房 `room12345678` 并打开底部菜单,确认原 Settings 入口已替换为 `Special effects management` 新图标入口。
|
||||
- [x] 已保存菜单验证截图:`build/voice_room_animation_switch_verification/ios17promax_room_menu_special_effects.png`。
|
||||
- [!] 继续点击新入口时 Computer Use 失去 Simulator 窗口句柄(`cgWindowNotFound`),无法继续通过 GUI 点开弹窗;已保留真实房间菜单验证截图,改用项目现有 debug preview 方式对同一个弹窗组件做 iPhone 17 Pro Max 截图验证。
|
||||
- [x] 已新增 `lib/debug/room_animation_switch_dialog_preview.dart`,复用同一个 `RoomAnimationSwitchDialog` 组件渲染 375x812 房间背景预览。
|
||||
- [x] 已分析弹窗预览入口,`flutter analyze --no-fatal-warnings --no-fatal-infos` 退出码 0;仍仅有既有 `rtc_manager.dart` / `rtm_manager.dart` warning/info。
|
||||
- [x] 已切换 iPhone 17 Pro Max 到弹窗预览入口,用于截图验证。
|
||||
- [!] 已截取首版弹窗预览图,发现英文标题在 375 宽下被截断为省略号;正在调整标题为两行可容纳布局并复截。
|
||||
- [x] 已热重载修正标题布局并复截弹窗图:`build/voice_room_animation_switch_verification/ios17promax_animation_switch_dialog_preview_v2.png`,标题、两行开关、按钮均未溢出。
|
||||
- [x] 已单独执行 `flutter analyze --no-fatal-warnings --no-fatal-infos lib/ui_kit/widgets/room/room_animation_switch_dialog.dart lib/debug/room_animation_switch_dialog_preview.dart`,结果 No issues found。
|
||||
- [x] 已退出弹窗预览运行,避免留下 `flutter run` 会话。
|
||||
- [x] 已检查最终变更范围:本轮核心改动集中在房间菜单、动画开关弹窗、礼物/进场特效播放判断与 debug 预览;工作区仍存在非本轮的 `floating_screen_manager.dart`、`pubspec.yaml`、首充弹窗相关文件改动,本轮不回退。
|
||||
|
||||
---
|
||||
|
||||
# CP 进度弹窗 Desktop MCP 复核进度
|
||||
|
||||
- [x] 2026-05-15 本轮重新执行语音房 Agora -> TRTC 迁移:确认 Flutter RTC 口径改为 TRTC `strRoomId = roomProfile.roomAccount`、`userId = 业务 userId`,不再使用 Agora `roomProfile.id` 频道号和数字 uid。
|
||||
|
||||