表情包 svga特效同步 语音房bug修复 VIP权益说明补充 背景 邀请码等
@ -603,6 +603,8 @@
|
||||
"custom": "مخصص",
|
||||
"customBackground": "خلفية مخصصة",
|
||||
"example": "مثال",
|
||||
"endPreview": "إنهاء المعاينة",
|
||||
"selectAgain": "اختر مرة أخرى",
|
||||
"store": "المتجر",
|
||||
"viewFrame": "عرض الإطار",
|
||||
"headdress": "إطارات",
|
||||
|
||||
@ -620,6 +620,8 @@
|
||||
"custom": "কাস্টম",
|
||||
"customBackground": "কাস্টম ব্যাকগ্রাউন্ড",
|
||||
"example": "উদাহরণ",
|
||||
"endPreview": "প্রিভিউ শেষ করুন",
|
||||
"selectAgain": "আবার নির্বাচন করুন",
|
||||
"myItems": "আমার আইটেম",
|
||||
"use": "ব্যবহার করুন",
|
||||
"unUse": "ব্যবহার না করুন",
|
||||
|
||||
@ -620,6 +620,8 @@
|
||||
"custom": "Custom",
|
||||
"customBackground": "Custom Background",
|
||||
"example": "Example",
|
||||
"endPreview": "End Preview",
|
||||
"selectAgain": "Select again",
|
||||
"myItems": "My items",
|
||||
"use": "Use",
|
||||
"unUse": "Unequip",
|
||||
|
||||
@ -620,7 +620,9 @@
|
||||
"custom": "Özel",
|
||||
"customBackground": "Özel Arka Plan",
|
||||
"example": "Örnek",
|
||||
"myItems": "Eşyalarım",
|
||||
"endPreview": "Önizlemeyi Bitir",
|
||||
"selectAgain": "Tekrar seç",
|
||||
"myItems": "Eşyalarım",
|
||||
"use": "Kullan",
|
||||
"unUse": "Kullanmama",
|
||||
"renewal": "Yenileme",
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:yumi/modules/chat/chat_route.dart';
|
||||
import 'package:yumi/modules/store/store_route.dart';
|
||||
import 'package:yumi/modules/index/index_page.dart';
|
||||
import 'package:yumi/modules/user/settings/settings_route.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/modules/auth/login_route.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
@ -15,17 +16,25 @@ import 'package:yumi/app/routes/sc_404_page.dart';
|
||||
class SCRoutes {
|
||||
static String home = '/';
|
||||
|
||||
static List<SCIRouterProvider> _listRouter = [];
|
||||
static final List<SCIRouterProvider> _listRouter = [];
|
||||
|
||||
static void configureRoutes(fluro.FluroRouter router) {
|
||||
/// 指定路由跳转错误返回页
|
||||
router.notFoundHandler = fluro.Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
|
||||
debugPrint('未找到目标页');
|
||||
return SCWidgetNotFound();
|
||||
});
|
||||
router.notFoundHandler = fluro.Handler(
|
||||
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
|
||||
debugPrint('未找到目标页');
|
||||
return SCWidgetNotFound();
|
||||
},
|
||||
);
|
||||
|
||||
router.define(home,
|
||||
handler: fluro.Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> params) => SCIndexPage()));
|
||||
router.define(
|
||||
home,
|
||||
handler: fluro.Handler(
|
||||
handlerFunc:
|
||||
(BuildContext? context, Map<String, List<String>> params) =>
|
||||
SCIndexPage(),
|
||||
),
|
||||
);
|
||||
|
||||
_listRouter.clear();
|
||||
|
||||
@ -37,6 +46,7 @@ class SCRoutes {
|
||||
_listRouter.add(SettingsRoute());
|
||||
_listRouter.add(VoiceRoomRoute());
|
||||
_listRouter.add(SCMainRoute());
|
||||
_listRouter.add(VipRoute());
|
||||
_listRouter.add(WalletRoute());
|
||||
_listRouter.add(StoreRoute());
|
||||
_listRouter.add(SCChatRouter());
|
||||
|
||||
@ -1326,6 +1326,10 @@ class SCAppLocalizations {
|
||||
|
||||
String get example => translate('example');
|
||||
|
||||
String get endPreview => translate('endPreview');
|
||||
|
||||
String get selectAgain => translate('selectAgain');
|
||||
|
||||
String get touristsCannotSendMessages =>
|
||||
translate('touristsCannotSendMessages');
|
||||
|
||||
|
||||
@ -604,13 +604,12 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
|
||||
context,
|
||||
listen: false,
|
||||
).uid;
|
||||
// TODO(register): When the backend registration API supports invitation
|
||||
// codes, pass `invitationCodeController.text.trim()` here as an optional
|
||||
// argument. Empty string should remain allowed.
|
||||
final invitationCode = invitationCodeController.text.trim();
|
||||
SocialChatLoginRes user = await SCAccountRepository().regist(
|
||||
authType,
|
||||
idToken,
|
||||
userProvider!.editUser!,
|
||||
invitationCode: invitationCode,
|
||||
);
|
||||
final submittedProfile = userProvider?.editUser;
|
||||
if (submittedProfile != null) {
|
||||
|
||||
434
lib/modules/room/background/room_background_preview_page.dart
Normal file
@ -0,0 +1,434 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart';
|
||||
|
||||
class RoomBackgroundPreviewPage extends StatelessWidget {
|
||||
const RoomBackgroundPreviewPage({super.key, this.backgroundPath});
|
||||
|
||||
final String? backgroundPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _PreviewBackground(path: backgroundPath)),
|
||||
Positioned.fill(child: _PreviewRoomLayer()),
|
||||
Positioned(
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
bottom: 148.w + bottomPadding,
|
||||
child: _PreviewPrimaryButton(
|
||||
text: localizations.endPreview,
|
||||
onTap: () => SCNavigatorUtils.goBackWithParams(context, true),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
bottom: 90.w + bottomPadding,
|
||||
child: _PreviewSecondaryButton(
|
||||
text: localizations.selectAgain,
|
||||
onTap: () => SCNavigatorUtils.goBackWithParams(context, false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewBackground extends StatelessWidget {
|
||||
const _PreviewBackground({this.path});
|
||||
|
||||
final String? path;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = (path ?? "").trim();
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
return Image.network(
|
||||
value,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(),
|
||||
);
|
||||
}
|
||||
if (value.startsWith("assets/") || value.startsWith("sc_images/")) {
|
||||
return Image.asset(
|
||||
value,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(),
|
||||
);
|
||||
}
|
||||
if (value.isNotEmpty) {
|
||||
final file = File(value);
|
||||
if (file.existsSync()) {
|
||||
return Image.file(
|
||||
file,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const _DefaultPreviewBackground(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const _DefaultPreviewBackground();
|
||||
}
|
||||
}
|
||||
|
||||
class _DefaultPreviewBackground extends StatelessWidget {
|
||||
const _DefaultPreviewBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getVoiceRoomDefaultBackgroundImage(),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewRoomLayer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: ScreenUtil().setWidth(42)),
|
||||
IgnorePointer(child: RoomHeadWidget()),
|
||||
SizedBox(height: 5.w),
|
||||
const IgnorePointer(child: RoomOnlineUserWidget()),
|
||||
const IgnorePointer(child: _PreviewSeat10()),
|
||||
Expanded(child: _PreviewChatArea()),
|
||||
const IgnorePointer(child: RoomBottomWidget()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSeat10 extends StatelessWidget {
|
||||
const _PreviewSeat10();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => Expanded(
|
||||
child: _PreviewSeatItem(index: index, locked: index == 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: List.generate(5, (index) {
|
||||
final seatIndex = index + 5;
|
||||
return Expanded(
|
||||
child: _PreviewSeatItem(
|
||||
index: seatIndex,
|
||||
locked: seatIndex == 6,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSeatItem extends StatelessWidget {
|
||||
const _PreviewSeatItem({required this.index, required this.locked});
|
||||
|
||||
final int index;
|
||||
final bool locked;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
child: Image.asset(
|
||||
locked
|
||||
? "sc_images/room/sc_icon_seat_lock.png"
|
||||
: "sc_images/room/sc_icon_seat_open.png",
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 16.w,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"NO.${index + 1}",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewChatArea extends StatefulWidget {
|
||||
@override
|
||||
State<_PreviewChatArea> createState() => _PreviewChatAreaState();
|
||||
}
|
||||
|
||||
class _PreviewChatAreaState extends State<_PreviewChatArea> {
|
||||
int _selectedTabIndex = 0;
|
||||
|
||||
void _selectTab(int index) {
|
||||
if (_selectedTabIndex == index) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedTabIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _PreviewChatTabs(
|
||||
selectedIndex: _selectedTabIndex,
|
||||
onTap: _selectTab,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
bottom: 166.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_PreviewChatBubble(text: "Hello My Friends!"),
|
||||
SizedBox(height: 12.w),
|
||||
_PreviewGiftBubble(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewChatTabs extends StatelessWidget {
|
||||
const _PreviewChatTabs({required this.selectedIndex, required this.onTap});
|
||||
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32.w,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => onTap(index),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
_assetFor(index, selectedIndex == index),
|
||||
height: 26.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _assetFor(int index, bool selected) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return selected
|
||||
? "sc_images/room/sc_icon_room_chat_tab_all_selected.png"
|
||||
: "sc_images/room/sc_icon_room_chat_tab_all_unselected.png";
|
||||
case 1:
|
||||
return selected
|
||||
? "sc_images/room/sc_icon_room_chat_tab_chat_selected.png"
|
||||
: "sc_images/room/sc_icon_room_chat_tab_chat_unselected.png";
|
||||
case 2:
|
||||
default:
|
||||
return selected
|
||||
? "sc_images/room/sc_icon_room_chat_tab_gift_selected.png"
|
||||
: "sc_images/room/sc_icon_room_chat_tab_gift_unselected.png";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewChatBubble extends StatelessWidget {
|
||||
const _PreviewChatBubble({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 276.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.42),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewGiftBubble extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 300.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.44),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"Send To User Name2",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
ClipOval(
|
||||
child: Image.asset(
|
||||
"sc_images/general/sc_icon_avar_defalt.png",
|
||||
width: 32.w,
|
||||
height: 32.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
"*1",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewPrimaryButton extends StatelessWidget {
|
||||
const _PreviewPrimaryButton({required this.text, required this.onTap});
|
||||
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
gradient: LinearGradient(
|
||||
colors: [SocialChatTheme.primaryLight, const Color(0xff8BF2D0)],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: const Color(0xff0B2823),
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSecondaryButton extends StatelessWidget {
|
||||
const _PreviewSecondaryButton({required this.text, required this.onTap});
|
||||
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff06372F).withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
border: Border.all(color: SocialChatTheme.primaryLight, width: 1.w),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -82,6 +82,17 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _previewItem(List<_RoomBackgroundItem> items, int index) async {
|
||||
final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview<bool>(
|
||||
context,
|
||||
backgroundPath: items[index].localImagePath,
|
||||
);
|
||||
if (!mounted || shouldUse != true) {
|
||||
return;
|
||||
}
|
||||
_selectItem(items, index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
@ -114,12 +125,12 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
_RoomBackgroundGrid(
|
||||
items: _officialItems,
|
||||
onTap:
|
||||
(index) => _selectItem(_officialItems, index),
|
||||
(index) => _previewItem(_officialItems, index),
|
||||
bottomPadding: 24.w,
|
||||
),
|
||||
_RoomBackgroundGrid(
|
||||
items: _mineItems,
|
||||
onTap: (index) => _selectItem(_mineItems, index),
|
||||
onTap: (index) => _previewItem(_mineItems, index),
|
||||
bottomPadding: 100.w,
|
||||
),
|
||||
],
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
@ -475,8 +476,8 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
}
|
||||
},
|
||||
)
|
||||
: netImage(
|
||||
url: playingEvent?.number ?? "",
|
||||
: _buildEmojiImage(
|
||||
playingEvent?.number ?? "",
|
||||
width: widget.isGameModel ? 65.w : 75.w,
|
||||
)),
|
||||
);
|
||||
@ -495,6 +496,21 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiImage(String path, {required double width}) {
|
||||
if (path.isEmpty) {
|
||||
return SizedBox(width: width, height: width);
|
||||
}
|
||||
if (path.startsWith("assets/") || path.startsWith("sc_images/")) {
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: width,
|
||||
);
|
||||
}
|
||||
return netImage(url: path, width: width);
|
||||
}
|
||||
|
||||
void _checkStart() {
|
||||
if (!showIn) {
|
||||
if (pathList.isNotEmpty) {
|
||||
|
||||
@ -67,6 +67,14 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
_enableRoomVisualEffects();
|
||||
_tabController.addListener(_handleTabChange);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.read<RtcProvider>().requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: true,
|
||||
);
|
||||
});
|
||||
|
||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||
event,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_preview_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_upload_page.dart';
|
||||
import 'package:yumi/modules/room/edit/room_edit_page.dart';
|
||||
@ -15,6 +16,7 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
static String roomEdit = '/room/roomEdit';
|
||||
static String roomTheme = '/room/roomTheme';
|
||||
static String roomBackgroundSelect = '/room/background/select';
|
||||
static String roomBackgroundPreview = '/room/background/preview';
|
||||
static String roomBackgroundUpload = '/room/background/upload';
|
||||
|
||||
static Route<T> _buildRoute<T>(Widget page) {
|
||||
@ -68,6 +70,16 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
).push<T>(_buildRoute<T>(const RoomBackgroundUploadPage()));
|
||||
}
|
||||
|
||||
static Future<T?> openRoomBackgroundPreview<T>(
|
||||
BuildContext context, {
|
||||
String? backgroundPath,
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
_buildRoute<T>(RoomBackgroundPreviewPage(backgroundPath: backgroundPath)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initRouter(FluroRouter router) {
|
||||
router.define(
|
||||
@ -84,6 +96,12 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
handlerFunc: (_, params) => const RoomBackgroundSelectPage(),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
roomBackgroundPreview,
|
||||
handler: Handler(
|
||||
handlerFunc: (_, params) => const RoomBackgroundPreviewPage(),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
roomBackgroundUpload,
|
||||
handler: Handler(
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/modules/store/store_route.dart';
|
||||
import 'package:yumi/modules/user/settings/settings_route.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
@ -79,7 +80,7 @@ class _MePage2State extends State<MePage2> {
|
||||
SizedBox(height: 12.w),
|
||||
_buildEntryRow(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildWalletCard(),
|
||||
_buildWalletVipRow(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildMenuCard2(),
|
||||
SizedBox(height: 12.w),
|
||||
@ -233,12 +234,21 @@ class _MePage2State extends State<MePage2> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletVipRow() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: _buildVipEntryCard()),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(child: _buildWalletCard()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletCard() {
|
||||
return SCDebounceWidget(
|
||||
onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 90.w,
|
||||
height: 68.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
image: const DecorationImage(
|
||||
@ -246,44 +256,53 @@ class _MePage2State extends State<MePage2> {
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.wallet,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.white,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
SizedBox(height: 2.w),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/general/sc_icon_jb.png',
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
'sc_images/general/sc_icon_wallet_coin.png',
|
||||
width: 18.w,
|
||||
height: 18.w,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
return Text(
|
||||
':${_balanceText(ref.myBalance)}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
);
|
||||
},
|
||||
SizedBox(width: 4.w),
|
||||
Expanded(
|
||||
child: Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
return Text(
|
||||
':${_balanceText(ref.myBalance)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -292,8 +311,8 @@ class _MePage2State extends State<MePage2> {
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/index/sc_icon_wallet_icon.png',
|
||||
width: 68.w,
|
||||
height: 68.w,
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -301,6 +320,103 @@ class _MePage2State extends State<MePage2> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipEntryCard() {
|
||||
return SCDebounceWidget(
|
||||
onTap: () => SCNavigatorUtils.push(context, VipRoute.detail),
|
||||
child: Container(
|
||||
height: 68.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(color: const Color(0xFFFFD37A), width: 1.w),
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('sc_images/vip/sc_vip_entry_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 10.w,
|
||||
offset: Offset(0, 4.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
top: 12.w,
|
||||
start: 14.w,
|
||||
child: Text(
|
||||
'VIP',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: 13.w,
|
||||
bottom: 10.w,
|
||||
child: _buildVipLevelMark(5),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 2.w,
|
||||
top: 8.w,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_badge_5.png',
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipLevelMark(int level) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_v.png',
|
||||
width: 18.1.w,
|
||||
height: 15.3.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_i.png',
|
||||
width: 8.3.w,
|
||||
height: 15.3.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_p.png',
|
||||
width: 18.1.w,
|
||||
height: 15.3.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
_buildVipLevelNumber(level),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipLevelNumber(int level) {
|
||||
return Image.asset(
|
||||
'sc_images/vip/sc_vip_number_${level}_filled.png',
|
||||
height: 15.247.w,
|
||||
fit: BoxFit.fill,
|
||||
color: const Color(0xFFFFD155),
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuCard2() {
|
||||
final userProfile = context.watch<SocialChatUserProfileManager>();
|
||||
final items = <_MenuRowData>[];
|
||||
@ -343,7 +459,7 @@ class _MePage2State extends State<MePage2> {
|
||||
if (userProfile.userIdentity?.bdLeader ?? false) {
|
||||
items.add(
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.bdLeader,
|
||||
title: SCAppLocalizations.of(context)!.admin,
|
||||
assetIcon: 'sc_images/index/sc_icon_bd_leader.png',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
|
||||
392
lib/modules/user/vip/vip_benefit_page.dart
Normal file
@ -0,0 +1,392 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
|
||||
class VipBenefitPage extends StatefulWidget {
|
||||
const VipBenefitPage({super.key, this.initialIndex = 0});
|
||||
|
||||
final int initialIndex;
|
||||
|
||||
@override
|
||||
State<VipBenefitPage> createState() => _VipBenefitPageState();
|
||||
}
|
||||
|
||||
class _VipBenefitPageState extends State<VipBenefitPage> {
|
||||
static const List<_VipBenefitData> _benefits = [
|
||||
_VipBenefitData(
|
||||
title: 'Gif Profile Picture',
|
||||
description: 'You can upload a Gif image as your profile picture',
|
||||
icon: Icons.image_outlined,
|
||||
),
|
||||
_VipBenefitData(
|
||||
title: 'Colorful Nickname',
|
||||
description: 'Your nickname can use a special colorful display effect',
|
||||
icon: Icons.notes_rounded,
|
||||
),
|
||||
_VipBenefitData(
|
||||
title: 'Gif Room Picture',
|
||||
description: 'You can use a Gif image as your room cover picture',
|
||||
icon: Icons.home_rounded,
|
||||
),
|
||||
_VipBenefitData(
|
||||
title: 'Colorful ID',
|
||||
description: 'Your user ID can use a highlighted colorful style',
|
||||
icon: Icons.pin_rounded,
|
||||
isIdIcon: true,
|
||||
),
|
||||
];
|
||||
|
||||
late int _selectedIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedIndex = widget.initialIndex.clamp(0, _benefits.length - 1).toInt();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selected = _benefits[_selectedIndex];
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)],
|
||||
stops: const [0, 0.40],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_page_bg.png',
|
||||
width: ScreenUtil().screenWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: 300.w,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.36),
|
||||
Colors.black.withValues(alpha: 0.62),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.52, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTopBar(context),
|
||||
SizedBox(height: 18.w),
|
||||
_buildBenefitTabs(),
|
||||
SizedBox(height: 36.w),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(36.w, 0, 36.w, 32.w),
|
||||
child: _buildBenefitCard(selected),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 44.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'VIP Benefits',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBenefitTabs() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
for (int index = 0; index < _benefits.length; index++)
|
||||
_buildBenefitTab(index, _benefits[index]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBenefitTab(int index, _VipBenefitData item) {
|
||||
final selected = index == _selectedIndex;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _selectedIndex = index),
|
||||
child: SizedBox(
|
||||
width: 58.w,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 54.w,
|
||||
height: 54.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
selected
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.transparent,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: _buildBenefitIcon(item, size: 28.w),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
opacity: selected ? 1 : 0,
|
||||
child: Container(
|
||||
width: 20.w,
|
||||
height: 3.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBenefitCard(_VipBenefitData item) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF171717),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(14.w, 22.w, 14.w, 14.w),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14.w),
|
||||
Text(
|
||||
item.description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.58),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.22,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
_buildPreviewPlaceholder(item),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewPlaceholder(_VipBenefitData item) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 260.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Color(0xFF234F73), Color(0xFFF5F5F5)],
|
||||
stops: const [0, 0.58],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 18.w,
|
||||
top: 16.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 24.w,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 80.w,
|
||||
child: Container(
|
||||
width: 64.w,
|
||||
height: 64.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.white, width: 2.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: _buildBenefitIcon(item, size: 34.w),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 110.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(18.w),
|
||||
topRight: Radius.circular(18.w),
|
||||
bottomLeft: Radius.circular(12.w),
|
||||
bottomRight: Radius.circular(12.w),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF333333),
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
Container(
|
||||
width: 88.w,
|
||||
height: 18.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
borderRadius: BorderRadius.circular(9.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Placeholder',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF555555),
|
||||
fontSize: 9.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBenefitIcon(_VipBenefitData item, {required double size}) {
|
||||
if (item.isIdIcon) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.w),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF7244), Color(0xFFFFD84E)],
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'ID',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: (size * 0.42).sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ShaderMask(
|
||||
shaderCallback:
|
||||
(bounds) => const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF6F3D), Color(0xFFFFD84E)],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Icon(item.icon, color: Colors.white, size: size),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VipBenefitData {
|
||||
const _VipBenefitData({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
this.isIdIcon = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final bool isIdIcon;
|
||||
}
|
||||
786
lib/modules/user/vip/vip_detail_page.dart
Normal file
@ -0,0 +1,786 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||
|
||||
class VipDetailPage extends StatefulWidget {
|
||||
const VipDetailPage({super.key});
|
||||
|
||||
@override
|
||||
State<VipDetailPage> createState() => _VipDetailPageState();
|
||||
}
|
||||
|
||||
class _VipDetailPageState extends State<VipDetailPage> {
|
||||
int _selectedLevel = 1;
|
||||
late bool _showUpdateButton;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showUpdateButton = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)],
|
||||
stops: const [0, 0.40],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_page_bg.png',
|
||||
width: ScreenUtil().screenWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: 300.w,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.36),
|
||||
Colors.black.withValues(alpha: 0.62),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.52, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTopBar(),
|
||||
_buildLevelTabs(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(12.w, 40.w, 12.w, 18.w),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildLevelCard(),
|
||||
SizedBox(height: 30.w),
|
||||
_buildLevelDivider(),
|
||||
SizedBox(height: 3.w),
|
||||
_buildSectionTitle('Decoration Privileges'),
|
||||
SizedBox(height: 10.w),
|
||||
_buildDecorationGrid(),
|
||||
SizedBox(height: 20.w),
|
||||
_buildSectionTitle('VIP Privileges'),
|
||||
SizedBox(height: 14.w),
|
||||
_buildPrivilegeGrid(),
|
||||
SizedBox(height: 76.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActivateBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar() {
|
||||
return SizedBox(
|
||||
height: 44.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'VIP',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.push(context, VipRoute.instruction),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 12.w),
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_question.png',
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelTabs() {
|
||||
return SizedBox(
|
||||
height: 43.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
itemCount: 5,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 30.w),
|
||||
itemBuilder: (context, index) {
|
||||
final level = index + 1;
|
||||
final selected = level == _selectedLevel;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _selectedLevel = level),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
'VIP$level',
|
||||
style: TextStyle(
|
||||
color:
|
||||
selected
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.42),
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
SizedBox(
|
||||
width: 0,
|
||||
height: 9.5.w,
|
||||
child: OverflowBox(
|
||||
minWidth: 60.w,
|
||||
maxWidth: 60.w,
|
||||
alignment: Alignment.center,
|
||||
child:
|
||||
selected
|
||||
? Image.asset(
|
||||
'sc_images/vip/sc_vip_selected_line.png',
|
||||
width: 60.w,
|
||||
height: 9.5.w,
|
||||
fit: BoxFit.fill,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelCard() {
|
||||
final assetLevel = _assetLevel;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 351.w,
|
||||
height: 124.5.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFEBCB76),
|
||||
width: 1.w,
|
||||
),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'sc_images/vip/sc_vip_card_bg_$assetLevel.png',
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: 20.w,
|
||||
top: 33.w,
|
||||
child: _buildVipLevelMark(_selectedLevel, scale: 1.18),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: 22.w,
|
||||
top: 80.w,
|
||||
child: Text(
|
||||
'Not activated',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFF2C766).withValues(alpha: 0.78),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 24.w,
|
||||
top: -36.w,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_badge_$assetLevel.png',
|
||||
width: 135.w,
|
||||
height: 135.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelDivider() {
|
||||
final ornamentLayout = _ornamentLayoutForLevel(_assetLevel);
|
||||
return SizedBox(
|
||||
height: 38.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
left: -12.w,
|
||||
right: -12.w,
|
||||
top: 0,
|
||||
height: 8.w,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_top_border_$_assetLevel.png',
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: ornamentLayout.top.w,
|
||||
child: Transform.translate(
|
||||
offset: Offset(ornamentLayout.dx.w, 0),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_card_ornament_$_assetLevel.png',
|
||||
width: ornamentLayout.width.w,
|
||||
height: ornamentLayout.height.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_title_left.png',
|
||||
width: 16.w,
|
||||
height: 20.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_title_right.png',
|
||||
width: 16.w,
|
||||
height: 20.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDecorationGrid() {
|
||||
final items = [
|
||||
_VipFeatureData(
|
||||
'SVIP Badge',
|
||||
'sc_images/vip/sc_vip_badge_$_assetLevel.png',
|
||||
),
|
||||
_VipFeatureData('Frame', null, Icons.person_pin_circle_outlined),
|
||||
_VipFeatureData('Mount', null, Icons.auto_awesome_motion),
|
||||
_VipFeatureData('Mic Animation', null, Icons.radio_button_checked),
|
||||
_VipFeatureData('Entry Effect', null, Icons.airline_seat_recline_extra),
|
||||
];
|
||||
return Wrap(
|
||||
spacing: 10.w,
|
||||
runSpacing: 10.w,
|
||||
children:
|
||||
items
|
||||
.map(
|
||||
(item) => _buildFeatureTile(
|
||||
item,
|
||||
width: item.title == 'Entry Effect' ? 206.w : 98.w,
|
||||
height: 100.w,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrivilegeGrid() {
|
||||
final items = [
|
||||
_VipFeatureData('Gif Profile\nPicture', null, Icons.badge_outlined),
|
||||
_VipFeatureData('Colorful\nNickname', null, Icons.credit_card),
|
||||
_VipFeatureData('Gif Room\nPicture', null, Icons.home_rounded),
|
||||
_VipFeatureData('Colorful ID', null, Icons.pin_rounded),
|
||||
];
|
||||
return Wrap(
|
||||
spacing: 34.w,
|
||||
runSpacing: 24.w,
|
||||
children:
|
||||
items.asMap().entries.map((entry) {
|
||||
final item = entry.value;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap:
|
||||
() => SCNavigatorUtils.push(
|
||||
context,
|
||||
'${VipRoute.benefit}?index=${entry.key}',
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 74.w,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 46.w,
|
||||
height: 46.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
item.icon,
|
||||
color: const Color(0xFFFF8E3D),
|
||||
size: 23.w,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
Text(
|
||||
item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewardPlaceholder(IconData icon) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 46.w,
|
||||
height: 46.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
const Color(0xFFFFD155).withValues(alpha: 0.28),
|
||||
Colors.white.withValues(alpha: 0.06),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD155).withValues(alpha: 0.45),
|
||||
width: 0.8.w,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: const Color(0xFFFFD155), size: 24.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWideRewardPlaceholder() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 40.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.w),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFFFFD155).withValues(alpha: 0.14),
|
||||
const Color(0xFF7F4DFF).withValues(alpha: 0.16),
|
||||
const Color(0xFFFFD155).withValues(alpha: 0.14),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD155).withValues(alpha: 0.35),
|
||||
width: 0.8.w,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
for (int i = 0; i < 5; i++)
|
||||
Container(
|
||||
width: 4.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureTile(
|
||||
_VipFeatureData item, {
|
||||
required double width,
|
||||
required double height,
|
||||
}) {
|
||||
final isWide = item.title == 'Entry Effect';
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.w),
|
||||
border: Border.all(color: const Color(0xFFEBCB76), width: 1.w),
|
||||
color: Colors.black.withValues(alpha: 0.38),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (item.assetPath != null)
|
||||
Expanded(child: Image.asset(item.assetPath!, fit: BoxFit.contain))
|
||||
else
|
||||
Expanded(
|
||||
child:
|
||||
isWide
|
||||
? Center(child: _buildWideRewardPlaceholder())
|
||||
: _buildRewardPlaceholder(
|
||||
item.icon ?? Icons.auto_awesome,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivateBar() {
|
||||
final bottomInset = MediaQuery.of(context).padding.bottom;
|
||||
final actionButton = _actionButtonConfigForLevel(_assetLevel);
|
||||
final barOverflow = 20.w + bottomInset;
|
||||
return SizedBox(
|
||||
height: 106.5.w,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: -barOverflow,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.w),
|
||||
topRight: Radius.circular(20.w),
|
||||
),
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_card_bg_$_assetLevel.png',
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
30.w,
|
||||
30.w,
|
||||
30.w,
|
||||
10.w + bottomInset,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 22.w,
|
||||
height: 22.w,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xFFFFA629),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.stars_rounded,
|
||||
color: Colors.white,
|
||||
size: 14.w,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
'30.5 M / 30 Days',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFE8A7),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 106.w,
|
||||
height: 42.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(actionButton.backgroundAssetPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
actionButton.label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: actionButton.textColor,
|
||||
fontFamily: actionButton.fontFamily,
|
||||
fontSize: actionButton.fontSize.sp,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipLevelMark(int level, {double scale = 1}) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_v.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_i.png',
|
||||
width: 14.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_p.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
_buildVipLevelNumber(level),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipLevelNumber(int level) {
|
||||
return Image.asset(
|
||||
'sc_images/vip/sc_vip_number_${level}_filled.png',
|
||||
height: 25.w,
|
||||
fit: BoxFit.fill,
|
||||
color: const Color(0xFFFFD155),
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
);
|
||||
}
|
||||
|
||||
_VipOrnamentLayout _ornamentLayoutForLevel(int level) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return const _VipOrnamentLayout(
|
||||
width: 176.5,
|
||||
height: 36,
|
||||
top: -10,
|
||||
dx: 0,
|
||||
);
|
||||
case 2:
|
||||
return const _VipOrnamentLayout(
|
||||
width: 246,
|
||||
height: 61,
|
||||
top: -20,
|
||||
dx: 0,
|
||||
);
|
||||
case 3:
|
||||
return const _VipOrnamentLayout(
|
||||
width: 277,
|
||||
height: 79.5,
|
||||
top: -35,
|
||||
dx: 0,
|
||||
);
|
||||
case 4:
|
||||
return const _VipOrnamentLayout(
|
||||
width: 240.5,
|
||||
height: 79.5,
|
||||
top: -35,
|
||||
dx: 0,
|
||||
);
|
||||
case 5:
|
||||
default:
|
||||
return const _VipOrnamentLayout(
|
||||
width: 282.5,
|
||||
height: 101.5,
|
||||
top: -48,
|
||||
dx: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_VipActionButtonConfig _actionButtonConfigForLevel(int level) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
default:
|
||||
return _showUpdateButton
|
||||
? const _VipActionButtonConfig.update()
|
||||
: const _VipActionButtonConfig.activate();
|
||||
}
|
||||
}
|
||||
|
||||
int get _assetLevel => _selectedLevel.clamp(1, 5).toInt();
|
||||
}
|
||||
|
||||
class _VipOrnamentLayout {
|
||||
const _VipOrnamentLayout({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.top,
|
||||
required this.dx,
|
||||
});
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
final double top;
|
||||
final double dx;
|
||||
}
|
||||
|
||||
class _VipFeatureData {
|
||||
const _VipFeatureData(this.title, this.assetPath, [this.icon]);
|
||||
|
||||
final String title;
|
||||
final String? assetPath;
|
||||
final IconData? icon;
|
||||
}
|
||||
|
||||
class _VipActionButtonConfig {
|
||||
const _VipActionButtonConfig({
|
||||
required this.label,
|
||||
required this.backgroundAssetPath,
|
||||
required this.textColor,
|
||||
required this.fontSize,
|
||||
required this.fontFamily,
|
||||
});
|
||||
|
||||
const _VipActionButtonConfig.activate()
|
||||
: this(
|
||||
label: 'Activate',
|
||||
backgroundAssetPath: 'sc_images/vip/sc_vip_activate_button_bg.png',
|
||||
textColor: const Color(0xFF7B4B16),
|
||||
fontSize: 14,
|
||||
fontFamily: null,
|
||||
);
|
||||
|
||||
const _VipActionButtonConfig.update()
|
||||
: this(
|
||||
label: 'Update',
|
||||
backgroundAssetPath: 'sc_images/vip/sc_vip_update_button_bg.png',
|
||||
textColor: const Color(0xFF740000),
|
||||
fontSize: 16,
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
);
|
||||
|
||||
final String label;
|
||||
final String backgroundAssetPath;
|
||||
final Color textColor;
|
||||
final double fontSize;
|
||||
final String? fontFamily;
|
||||
}
|
||||
491
lib/modules/user/vip/vip_instruction_page.dart
Normal file
@ -0,0 +1,491 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
|
||||
class VipInstructionPage extends StatefulWidget {
|
||||
const VipInstructionPage({super.key});
|
||||
|
||||
@override
|
||||
State<VipInstructionPage> createState() => _VipInstructionPageState();
|
||||
}
|
||||
|
||||
class _VipInstructionPageState extends State<VipInstructionPage> {
|
||||
_VipInstructionTab _selectedTab = _VipInstructionTab.record;
|
||||
|
||||
static const List<_VipRecordRow> _recordRows = [
|
||||
_VipRecordRow(
|
||||
level: 1,
|
||||
validDays: '30days',
|
||||
type: 'Expiration',
|
||||
time: '21/09/2029\n12:99:00',
|
||||
),
|
||||
_VipRecordRow(
|
||||
level: 2,
|
||||
validDays: '10days',
|
||||
type: 'Card',
|
||||
time: '21/09/2029\n12:99:00',
|
||||
),
|
||||
_VipRecordRow(
|
||||
level: 3,
|
||||
validDays: '30days',
|
||||
type: 'Renew',
|
||||
time: '21/09/2029\n12:99:00',
|
||||
),
|
||||
_VipRecordRow(
|
||||
level: 4,
|
||||
validDays: '30days',
|
||||
type: 'Purchase',
|
||||
time: '21/09/2029\n12:99:00',
|
||||
),
|
||||
_VipRecordRow(
|
||||
level: 5,
|
||||
validDays: '30days',
|
||||
type: 'Upgrade',
|
||||
time: '21/09/2029\n12:99:00',
|
||||
),
|
||||
];
|
||||
|
||||
static const List<_VipDescriptionSection> _descriptionSections = [
|
||||
_VipDescriptionSection(
|
||||
'Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.',
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'You can upgrade to a higher VIP level by paying the price difference. After upgrading, a new 30-day validity period will be given.',
|
||||
bullets: [
|
||||
'Price Difference = Target Price - Remaining Price for Current Days.',
|
||||
'The upgrade takes effect immediately, and the remaining time of the previous level will be overwritten.',
|
||||
],
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'Once a higher-level VIP is activated, it is not possible to purchase a lower-level VIP.',
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'Different levels cannot be stacked simultaneously; only one level can be activated at a time.',
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'If using a card from your inventory to activate a new level:',
|
||||
bullets: [
|
||||
'If the new level is higher than the current level, it will directly upgrade, and the remaining time of the previous level will be cleared.',
|
||||
'If the new level is lower than the current level, activation cannot be used.',
|
||||
],
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'After the VIP expires, the corresponding privileges will be lost, but the purchase records will not be cleared.',
|
||||
),
|
||||
_VipDescriptionSection(
|
||||
'The platform reserves the final right to interpret the VIP rules.',
|
||||
),
|
||||
];
|
||||
|
||||
static const Color _tableLightColor = Color(0xFF262626);
|
||||
static const Color _tableDarkColor = Color(0xFF191919);
|
||||
static const Color _tableDividerColor = Color(0xFF333333);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)],
|
||||
stops: const [0, 0.40],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_page_bg.png',
|
||||
width: ScreenUtil().screenWidth,
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: 300.w,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.36),
|
||||
Colors.black.withValues(alpha: 0.62),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.52, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTopBar(context),
|
||||
SizedBox(height: 6.w),
|
||||
_buildTabs(),
|
||||
Expanded(
|
||||
child:
|
||||
_selectedTab == _VipInstructionTab.record
|
||||
? _buildRecordView()
|
||||
: _buildDescriptionView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 44.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'VIP Rules',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabs() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildTab(_VipInstructionTab.record, 'Record'),
|
||||
SizedBox(width: 45.5.w),
|
||||
_buildTab(_VipInstructionTab.description, 'Description'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(_VipInstructionTab tab, String title) {
|
||||
final selected = _selectedTab == tab;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _selectedTab = tab),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color:
|
||||
selected ? Colors.white : Colors.white.withValues(alpha: 0.5),
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
fontSize: 18.sp,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
opacity: selected ? 1 : 0,
|
||||
child: Container(
|
||||
width: 18.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordView() {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 24.w),
|
||||
child: _buildRecordTable(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordTable() {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
child: Container(
|
||||
color: _tableDarkColor,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTableRow(
|
||||
const ['Level', 'Valid Days', 'type', 'Time'],
|
||||
height: 38.5.w,
|
||||
isHeader: true,
|
||||
isLastRow: false,
|
||||
),
|
||||
for (int i = 0; i < _recordRows.length; i++)
|
||||
_buildRecordDataRow(
|
||||
_recordRows[i],
|
||||
isLastRow: i == _recordRows.length - 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableRow(
|
||||
List<String> values, {
|
||||
required double height,
|
||||
required bool isHeader,
|
||||
required bool isLastRow,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Row(
|
||||
children: List.generate(values.length, (index) {
|
||||
return _buildTableCell(
|
||||
columnIndex: index,
|
||||
isLastColumn: index == values.length - 1,
|
||||
isLastRow: isLastRow,
|
||||
flex: _columnFlex(index),
|
||||
child: Text(
|
||||
values[index],
|
||||
textAlign: TextAlign.center,
|
||||
style: _tableTextStyle,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordDataRow(_VipRecordRow row, {required bool isLastRow}) {
|
||||
return SizedBox(
|
||||
height: 76.w,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTableCell(
|
||||
columnIndex: 0,
|
||||
isLastColumn: false,
|
||||
isLastRow: isLastRow,
|
||||
flex: _columnFlex(0),
|
||||
child: Image.asset(
|
||||
'sc_images/vip/sc_vip_badge_${row.level}.png',
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
_buildTableCell(
|
||||
columnIndex: 1,
|
||||
isLastColumn: false,
|
||||
isLastRow: isLastRow,
|
||||
flex: _columnFlex(1),
|
||||
child: Text(
|
||||
row.validDays,
|
||||
textAlign: TextAlign.center,
|
||||
style: _tableTextStyle,
|
||||
),
|
||||
),
|
||||
_buildTableCell(
|
||||
columnIndex: 2,
|
||||
isLastColumn: false,
|
||||
isLastRow: isLastRow,
|
||||
flex: _columnFlex(2),
|
||||
child: Text(
|
||||
row.type,
|
||||
textAlign: TextAlign.center,
|
||||
style: _tableTextStyle,
|
||||
),
|
||||
),
|
||||
_buildTableCell(
|
||||
columnIndex: 3,
|
||||
isLastColumn: true,
|
||||
isLastRow: isLastRow,
|
||||
flex: _columnFlex(3),
|
||||
child: Text(
|
||||
row.time,
|
||||
textAlign: TextAlign.center,
|
||||
style: _tableTextStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableCell({
|
||||
required int columnIndex,
|
||||
required bool isLastColumn,
|
||||
required bool isLastRow,
|
||||
required int flex,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Expanded(
|
||||
flex: flex,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: columnIndex.isEven ? _tableLightColor : _tableDarkColor,
|
||||
border: Border(
|
||||
right:
|
||||
isLastColumn
|
||||
? BorderSide.none
|
||||
: BorderSide(color: _tableDividerColor, width: 1.w),
|
||||
bottom:
|
||||
isLastRow
|
||||
? BorderSide.none
|
||||
: BorderSide(color: _tableDividerColor, width: 1.w),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionView() {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(16.w, 18.w, 16.w, 32.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < _descriptionSections.length; i++) ...[
|
||||
_buildDescriptionSection(i + 1, _descriptionSections[i]),
|
||||
SizedBox(height: 18.w),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionSection(int index, _VipDescriptionSection section) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$index. ${section.text}', style: _descriptionTextStyle),
|
||||
if (section.bullets.isNotEmpty) ...[
|
||||
SizedBox(height: 8.w),
|
||||
for (final bullet in section.bullets) ...[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 18.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('•', style: _descriptionHighlightStyle),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(bullet, style: _descriptionHighlightStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int _columnFlex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return 92;
|
||||
case 1:
|
||||
return 100;
|
||||
case 2:
|
||||
return 114;
|
||||
case 3:
|
||||
default:
|
||||
return 110;
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle get _tableTextStyle => TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
fontSize: 12.sp,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 14.5 / 12,
|
||||
);
|
||||
|
||||
TextStyle get _descriptionTextStyle => TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
fontSize: 16.sp,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 14.5 / 12,
|
||||
);
|
||||
|
||||
TextStyle get _descriptionHighlightStyle => TextStyle(
|
||||
color: const Color(0xFFFF9830),
|
||||
fontFamily: 'Source Han Sans SC',
|
||||
fontSize: 16.sp,
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 14.5 / 12,
|
||||
);
|
||||
}
|
||||
|
||||
enum _VipInstructionTab { record, description }
|
||||
|
||||
class _VipRecordRow {
|
||||
const _VipRecordRow({
|
||||
required this.level,
|
||||
required this.validDays,
|
||||
required this.type,
|
||||
required this.time,
|
||||
});
|
||||
|
||||
final int level;
|
||||
final String validDays;
|
||||
final String type;
|
||||
final String time;
|
||||
}
|
||||
|
||||
class _VipDescriptionSection {
|
||||
const _VipDescriptionSection(this.text, {this.bullets = const []});
|
||||
|
||||
final String text;
|
||||
final List<String> bullets;
|
||||
}
|
||||
32
lib/modules/user/vip/vip_route.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:yumi/app/routes/sc_router_init.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_benefit_page.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_detail_page.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_instruction_page.dart';
|
||||
|
||||
class VipRoute implements SCIRouterProvider {
|
||||
static String detail = '/main/me/vip/detail';
|
||||
static String instruction = '/main/me/vip/instruction';
|
||||
static String benefit = '/main/me/vip/benefit';
|
||||
|
||||
@override
|
||||
void initRouter(FluroRouter router) {
|
||||
router.define(
|
||||
detail,
|
||||
handler: Handler(handlerFunc: (_, params) => const VipDetailPage()),
|
||||
);
|
||||
router.define(
|
||||
instruction,
|
||||
handler: Handler(handlerFunc: (_, params) => const VipInstructionPage()),
|
||||
);
|
||||
router.define(
|
||||
benefit,
|
||||
handler: Handler(
|
||||
handlerFunc:
|
||||
(_, params) => VipBenefitPage(
|
||||
initialIndex: int.tryParse(params['index']?.first ?? '') ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool _isRefreshingOnlineUsers = false;
|
||||
bool _disableMicListRefreshForCurrentSession = false;
|
||||
bool _disableOnlineUsersRefreshForCurrentSession = false;
|
||||
bool _isSelfMicActionInFlight = false;
|
||||
bool _pendingMicListRefresh = false;
|
||||
Timer? _deferredMicListRefreshTimer;
|
||||
int _lastMicListRefreshStartedAtMs = 0;
|
||||
@ -192,6 +193,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
bool _isCancelledRequest(Object error) {
|
||||
if (error is DioException) {
|
||||
return error.type == DioExceptionType.cancel ||
|
||||
(error.error?.toString().trim().toLowerCase() == 'cancel');
|
||||
}
|
||||
final normalizedMessage = error.toString().toLowerCase();
|
||||
return normalizedMessage.contains('dioexceptiontype.cancel') ||
|
||||
normalizedMessage.contains('error: cancel') ||
|
||||
normalizedMessage.contains('request was manually cancelled');
|
||||
}
|
||||
|
||||
bool _sameOnlineUsers(
|
||||
List<SocialChatUserProfile> previous,
|
||||
List<SocialChatUserProfile> next,
|
||||
@ -1364,6 +1376,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_resetLocalAudioRuntimeTracking();
|
||||
_disableMicListRefreshForCurrentSession = false;
|
||||
_disableOnlineUsersRefreshForCurrentSession = false;
|
||||
_isSelfMicActionInFlight = false;
|
||||
roomRocketStatus = null;
|
||||
rtmProvider
|
||||
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
|
||||
@ -1426,7 +1439,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user;
|
||||
SocialChatUserProfile? roomWheatUser = micAtIndexForDisplay(index)?.user;
|
||||
showBottomInBottomDialog(
|
||||
context!,
|
||||
EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
||||
@ -1474,7 +1487,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
final seat = roomWheatMap[index];
|
||||
final seat = micAtIndexForDisplay(index);
|
||||
final seatUser = seat?.user;
|
||||
if (seatUser == null) {
|
||||
if (!(seat?.micLock ?? false) && !isRoomAdmin) {
|
||||
@ -1550,6 +1563,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (_isSelfMicActionInFlight) {
|
||||
return;
|
||||
}
|
||||
_isSelfMicActionInFlight = true;
|
||||
try {
|
||||
var micGoUpRes = await SCChatRoomRepository().micGoUp(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
@ -1608,6 +1625,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
}
|
||||
_syncSelfMicRuntimeState();
|
||||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
if (isFz()) {
|
||||
@ -1618,11 +1636,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
_refreshMicListSilently();
|
||||
} catch (ex) {
|
||||
SCTts.show('Failed to put on the microphone, $ex');
|
||||
if (_isCancelledRequest(ex)) {
|
||||
_refreshMicListSilently();
|
||||
return;
|
||||
}
|
||||
SCTts.show('Failed to put on the microphone, ${_errorMessageFrom(ex)}');
|
||||
} finally {
|
||||
_isSelfMicActionInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
xiaMai(num index) async {
|
||||
if (_isSelfMicActionInFlight) {
|
||||
return;
|
||||
}
|
||||
_isSelfMicActionInFlight = true;
|
||||
try {
|
||||
await SCChatRoomRepository().micGoDown(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
@ -1648,13 +1676,20 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
engine?.renewToken("");
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
_clearUserFromSeats(currentUserId);
|
||||
_syncSelfMicRuntimeState();
|
||||
notifyListeners();
|
||||
requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: false,
|
||||
minInterval: const Duration(milliseconds: 350),
|
||||
);
|
||||
} catch (ex) {
|
||||
SCTts.show('Failed to leave the microphone, $ex');
|
||||
if (_isCancelledRequest(ex)) {
|
||||
_refreshMicListSilently();
|
||||
return;
|
||||
}
|
||||
SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}');
|
||||
} finally {
|
||||
_isSelfMicActionInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -916,7 +916,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.image) {
|
||||
} else if (msg.type == SCRoomMsgType.image ||
|
||||
(msg.type == SCRoomMsgType.emoticons && (msg.number ?? -1) < 0)) {
|
||||
roomChatMsgList.insert(0, msg);
|
||||
if (roomChatMsgList.length > 250) {
|
||||
print('大于200条消息');
|
||||
@ -1431,11 +1432,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.emoticons) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).starPlayEmoji(msg);
|
||||
return;
|
||||
if ((msg.number ?? -1) >= 0) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).starPlayEmoji(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.micChange) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
|
||||
@ -44,7 +44,7 @@ abstract class SocialChatUserRepository {
|
||||
String openId,
|
||||
SCUserProfileCmd userProfileCmd, {
|
||||
SCMobileAuthCmd? mobileAuthCmd,
|
||||
String invitePeople,
|
||||
String invitationCode,
|
||||
});
|
||||
|
||||
///关注的房间
|
||||
|
||||
@ -340,8 +340,13 @@ class BaseNetworkClient {
|
||||
response: e.response,
|
||||
error: '服务器错误: ${e.response?.data["errorCode"]}',
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(requestOptions: e.requestOptions, error: 'Cancel');
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
type: DioExceptionType.cancel,
|
||||
error: e.error ?? 'Cancel',
|
||||
);
|
||||
default:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
|
||||
@ -60,18 +60,16 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
String openId,
|
||||
SCUserProfileCmd userProfileCmd, {
|
||||
SCMobileAuthCmd? mobileAuthCmd,
|
||||
String invitePeople = "",
|
||||
String invitationCode = "",
|
||||
}) async {
|
||||
var param = {};
|
||||
param["type"] = type;
|
||||
param["openId"] = openId;
|
||||
param["profile"] = userProfileCmd.toJson();
|
||||
param["invitationCode"] = invitationCode;
|
||||
if (mobileAuthCmd != null) {
|
||||
param["mobile"] = mobileAuthCmd.toJson();
|
||||
}
|
||||
if (invitePeople.isNotEmpty) {
|
||||
param["invitePeople"] = invitePeople;
|
||||
}
|
||||
final result = await http.post(
|
||||
"aa35848b37e5ba74d93e5a1f719c579cb289b5940c15040473b6f57550de35df",
|
||||
data: param,
|
||||
|
||||
@ -40,30 +40,20 @@ class RoomBottomChatEntry extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 14.w, end: 14.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/room/icon_room_input_t.png',
|
||||
width: 22.w,
|
||||
height: 22.w,
|
||||
fit: BoxFit.contain,
|
||||
padding: EdgeInsetsDirectional.only(start: 16.w, end: 16.w),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
localizations?.translate('roomBottomGreeting') ?? 'Hi...',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.0,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations?.translate('roomBottomGreeting') ?? 'Hi...',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
|
||||
class LuckGiftNomorAnimWidget extends StatefulWidget {
|
||||
@ -18,22 +23,9 @@ class LuckGiftNomorAnimWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
static const double _groupShiftXRatio = 26 / 422;
|
||||
static const double _groupShiftYRatio = 18 / 422;
|
||||
static const double _groupShiftYExtraUnits = 40;
|
||||
static const double _avatarScale = 0.8;
|
||||
static const double _avatarExtraShiftXUnits = -2;
|
||||
static const double _avatarExtraShiftYUnits = 18;
|
||||
static const double _avatarDiameterRatio = 76 / 422;
|
||||
static const double _avatarLeftRatio = 152 / 422;
|
||||
static const double _avatarTopRatio = 64 / 422;
|
||||
static const double _awardTopRatio = 153 / 422;
|
||||
static const double _awardXOffsetRatio = -13 / 422;
|
||||
static const double _multipleTopRatio = 193 / 422;
|
||||
static const double _multipleXOffsetRatio = -22 / 422;
|
||||
|
||||
String? _currentBurstEventId;
|
||||
bool _isRewardAmountVisible = false;
|
||||
static const String _avatarDynamicKey = "avatar";
|
||||
static const String _awardAmountDynamicKey = "digit01";
|
||||
static const String _multipleDynamicKey = "digit02";
|
||||
|
||||
bool _shouldPlayRewardBurst(Data rewardData) {
|
||||
return RealTimeMessagingManager.shouldPlayLuckyGiftBurst(rewardData);
|
||||
@ -56,38 +48,201 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
return multiple.toString();
|
||||
}
|
||||
|
||||
Widget _buildSenderAvatar({
|
||||
required String avatarUrl,
|
||||
required double outerSize,
|
||||
}) {
|
||||
final avatarPadding = outerSize * 0.045;
|
||||
final innerSize = outerSize - avatarPadding * 2;
|
||||
return RepaintBoundary(
|
||||
child: Container(
|
||||
width: outerSize,
|
||||
height: outerSize,
|
||||
padding: EdgeInsets.all(avatarPadding),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFFFD680),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x55243A74),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 6),
|
||||
Future<void> _configureRewardBurstSvga(
|
||||
MovieEntity movieEntity,
|
||||
Data rewardData,
|
||||
) async {
|
||||
final avatarUrl = (rewardData.userAvatar ?? '').trim();
|
||||
if (avatarUrl.isNotEmpty) {
|
||||
try {
|
||||
final avatarImage = await _buildAvatarLayerImage(avatarUrl);
|
||||
if (avatarImage != null) {
|
||||
movieEntity.dynamicItem.setImage(avatarImage, _avatarDynamicKey);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('[LuckGiftBurstSVGA] avatar sync failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final awardAmountImage = await _buildTextLayerImage(
|
||||
text: _formatAwardAmount(rewardData.awardAmount ?? 0),
|
||||
logicalWidth: 140,
|
||||
logicalHeight: 65,
|
||||
fontSize: 42,
|
||||
color: const Color(0xFFFFF3B6),
|
||||
shadowColor: const Color(0xCC7A3E00),
|
||||
);
|
||||
movieEntity.dynamicItem.setImage(
|
||||
awardAmountImage,
|
||||
_awardAmountDynamicKey,
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('[LuckGiftBurstSVGA] award amount sync failed: $error');
|
||||
}
|
||||
|
||||
final multiple = rewardData.multiple ?? 0;
|
||||
if (multiple > 0) {
|
||||
try {
|
||||
final multipleImage = await _buildTextLayerImage(
|
||||
text: 'x${_formatMultiple(multiple)}',
|
||||
logicalWidth: 166,
|
||||
logicalHeight: 37,
|
||||
fontSize: 28,
|
||||
color: const Color(0xFFF4F6FF),
|
||||
shadowColor: const Color(0x990B2D72),
|
||||
);
|
||||
movieEntity.dynamicItem.setImage(multipleImage, _multipleDynamicKey);
|
||||
} catch (error) {
|
||||
debugPrint('[LuckGiftBurstSVGA] multiple sync failed: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.Image?> _buildAvatarLayerImage(String avatarUrl) async {
|
||||
final avatarImage = await _loadUiImage(
|
||||
avatarUrl,
|
||||
logicalWidth: 100,
|
||||
logicalHeight: 100,
|
||||
);
|
||||
if (avatarImage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const double size = 100;
|
||||
const double pixelRatio = 3;
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
canvas.scale(pixelRatio, pixelRatio);
|
||||
final rect = Rect.fromLTWH(0, 0, size, size);
|
||||
final center = rect.center;
|
||||
|
||||
canvas.drawCircle(
|
||||
center,
|
||||
size / 2,
|
||||
Paint()
|
||||
..color = const Color(0xFFFFD680)
|
||||
..isAntiAlias = true,
|
||||
);
|
||||
canvas.save();
|
||||
canvas.clipPath(Path()..addOval(rect.deflate(5)));
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: rect.deflate(5),
|
||||
image: avatarImage,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
);
|
||||
canvas.restore();
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
return picture.toImage(
|
||||
(size * pixelRatio).round(),
|
||||
(size * pixelRatio).round(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Image> _buildTextLayerImage({
|
||||
required String text,
|
||||
required double logicalWidth,
|
||||
required double logicalHeight,
|
||||
required double fontSize,
|
||||
required Color color,
|
||||
required Color shadowColor,
|
||||
}) async {
|
||||
const double pixelRatio = 3;
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
canvas.scale(pixelRatio, pixelRatio);
|
||||
canvas.clipRect(Rect.fromLTWH(0, 0, logicalWidth, logicalHeight));
|
||||
|
||||
final textPainter = TextPainter(
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: shadowColor,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: netImage(
|
||||
url: avatarUrl,
|
||||
width: innerSize,
|
||||
height: innerSize,
|
||||
fit: BoxFit.cover,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
)..layout();
|
||||
|
||||
final maxTextWidth = logicalWidth - 8;
|
||||
final maxTextHeight = logicalHeight - 6;
|
||||
final scale = math.min(
|
||||
1.0,
|
||||
math.min(
|
||||
maxTextWidth / math.max(textPainter.width, 1),
|
||||
maxTextHeight / math.max(textPainter.height, 1),
|
||||
),
|
||||
);
|
||||
canvas.save();
|
||||
canvas.translate(logicalWidth / 2, logicalHeight / 2);
|
||||
canvas.scale(scale, scale);
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(-textPainter.width / 2, -textPainter.height / 2),
|
||||
);
|
||||
canvas.restore();
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
return picture.toImage(
|
||||
(logicalWidth * pixelRatio).round(),
|
||||
(logicalHeight * pixelRatio).round(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Image?> _loadUiImage(
|
||||
String resource, {
|
||||
double? logicalWidth,
|
||||
double? logicalHeight,
|
||||
}) async {
|
||||
final target = resource.trim();
|
||||
if (target.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final provider = buildCachedImageProvider(
|
||||
target,
|
||||
logicalWidth: logicalWidth,
|
||||
logicalHeight: logicalHeight,
|
||||
);
|
||||
final stream = provider.resolve(ImageConfiguration.empty);
|
||||
final completer = Completer<ui.Image?>();
|
||||
late final ImageStreamListener listener;
|
||||
listener = ImageStreamListener(
|
||||
(imageInfo, synchronousCall) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(imageInfo.image);
|
||||
}
|
||||
stream.removeListener(listener);
|
||||
},
|
||||
onError: (Object error, StackTrace? stackTrace) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(null);
|
||||
}
|
||||
stream.removeListener(listener);
|
||||
},
|
||||
);
|
||||
stream.addListener(listener);
|
||||
return completer.future.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
stream.removeListener(listener);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -98,32 +253,8 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
builder: (context, provider, child) {
|
||||
final rewardData = provider.currentPlayingLuckGift?.data;
|
||||
if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) {
|
||||
_currentBurstEventId = null;
|
||||
_isRewardAmountVisible = false;
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final screenWidth = ScreenUtil().screenWidth;
|
||||
final groupShiftX = screenWidth * _groupShiftXRatio;
|
||||
final groupShiftY =
|
||||
screenWidth * _groupShiftYRatio + _groupShiftYExtraUnits.w;
|
||||
final avatarBaseDiameter = screenWidth * _avatarDiameterRatio;
|
||||
final avatarDiameter = avatarBaseDiameter * _avatarScale;
|
||||
final avatarInset = (avatarBaseDiameter - avatarDiameter) / 2;
|
||||
final avatarLeft =
|
||||
screenWidth * _avatarLeftRatio +
|
||||
groupShiftX +
|
||||
avatarInset +
|
||||
_avatarExtraShiftXUnits.w;
|
||||
final avatarTop =
|
||||
screenWidth * _avatarTopRatio +
|
||||
groupShiftY +
|
||||
avatarInset +
|
||||
_avatarExtraShiftYUnits.w;
|
||||
final awardTop = screenWidth * _awardTopRatio + groupShiftY;
|
||||
final awardXOffset = screenWidth * _awardXOffsetRatio + groupShiftX;
|
||||
final multipleTop = screenWidth * _multipleTopRatio + groupShiftY;
|
||||
final multipleXOffset =
|
||||
screenWidth * _multipleXOffsetRatio + groupShiftX;
|
||||
final rewardAnimationKey = ValueKey<String>(
|
||||
'${rewardData.sendUserId ?? ""}'
|
||||
'|${rewardData.acceptUserId ?? ""}'
|
||||
@ -131,132 +262,23 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
'|${rewardData.multiple ?? 0}'
|
||||
'|${rewardData.normalizedMultipleType}',
|
||||
);
|
||||
if (_currentBurstEventId != rewardAnimationKey.value) {
|
||||
_currentBurstEventId = rewardAnimationKey.value;
|
||||
_isRewardAmountVisible = false;
|
||||
}
|
||||
return SizedBox(
|
||||
height: 380.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10.w),
|
||||
child: SCSvgaAssetWidget(
|
||||
key: ValueKey<String>('burst|${rewardAnimationKey.value}'),
|
||||
assetPath: LuckGiftNomorAnimWidget._rewardBurstAssetPath,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 380.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
allowDrawingOverflow: true,
|
||||
clearsAfterStop: true,
|
||||
onPlaybackStarted: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (!_isRewardAmountVisible) {
|
||||
setState(() {
|
||||
_isRewardAmountVisible = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPlaybackCompleted: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (_isRewardAmountVisible) {
|
||||
setState(() {
|
||||
_isRewardAmountVisible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isRewardAmountVisible &&
|
||||
(rewardData.userAvatar ?? '').trim().isNotEmpty)
|
||||
Positioned(
|
||||
left: avatarLeft,
|
||||
top: avatarTop,
|
||||
child: _buildSenderAvatar(
|
||||
avatarUrl: (rewardData.userAvatar ?? '').trim(),
|
||||
outerSize: avatarDiameter,
|
||||
),
|
||||
),
|
||||
if (_isRewardAmountVisible)
|
||||
Positioned(
|
||||
top: awardTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: Offset(awardXOffset, 0),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: screenWidth * 0.34,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
_formatAwardAmount(rewardData.awardAmount ?? 0),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 28.sp,
|
||||
color: const Color(0xFFFFF3B6),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color(0xCC7A3E00),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isRewardAmountVisible && (rewardData.multiple ?? 0) > 0)
|
||||
Positioned(
|
||||
top: multipleTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: Offset(multipleXOffset, 0),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: screenWidth * 0.18,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'x${_formatMultiple(rewardData.multiple ?? 0)}',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 17.sp,
|
||||
color: const Color(0xFFF4F6FF),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color(0x990B2D72),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 10.w),
|
||||
child: SCSvgaAssetWidget(
|
||||
key: ValueKey<String>('burst|${rewardAnimationKey.value}'),
|
||||
assetPath: LuckGiftNomorAnimWidget._rewardBurstAssetPath,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 380.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
allowDrawingOverflow: true,
|
||||
clearsAfterStop: true,
|
||||
dynamicIdentity: rewardAnimationKey.value,
|
||||
movieConfigurer:
|
||||
(movieEntity) =>
|
||||
_configureRewardBurstSvga(movieEntity, rewardData),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@ -70,7 +70,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MicRes? userWheat = provider.roomWheatMap[index];
|
||||
MicRes? userWheat = provider.micAtIndexForDisplay(index);
|
||||
if (userWheat?.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
if (provider.isFz()) {
|
||||
@ -248,7 +248,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
showBottomInCenterDialog(
|
||||
context!,
|
||||
context,
|
||||
RoomUserInfoCard(userId: clickUser?.id),
|
||||
);
|
||||
},
|
||||
|
||||
@ -37,6 +37,8 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
static const double _floatingButtonBottomOffset = 54;
|
||||
static const double _giftActionWidth = 52;
|
||||
static const double _circleActionWidth = 46;
|
||||
static const double _emojiActionWidth = 38;
|
||||
static const double _chatEmojiGap = 8;
|
||||
static const double _compactGap = 14;
|
||||
static const double _horizontalPadding = 16;
|
||||
|
||||
@ -59,7 +61,10 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
builder: (context, bottomSnapshot, child) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = constraints.maxWidth / 3;
|
||||
final inputWidth = _resolveInputWidth(
|
||||
maxWidth: constraints.maxWidth,
|
||||
showMic: bottomSnapshot.showMic,
|
||||
);
|
||||
final giftCenterX = _resolveGiftCenterX(
|
||||
maxWidth: constraints.maxWidth,
|
||||
inputWidth: inputWidth,
|
||||
@ -107,6 +112,17 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChatControls(double inputWidth) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildChatEntry(inputWidth),
|
||||
SizedBox(width: _chatEmojiGap.w),
|
||||
_buildEmojiAction(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar({
|
||||
required double inputWidth,
|
||||
required bool showMic,
|
||||
@ -127,7 +143,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildChatEntry(inputWidth),
|
||||
_buildChatControls(inputWidth),
|
||||
giftAction,
|
||||
_buildMicAction(isMic: isMic),
|
||||
menuAction,
|
||||
@ -137,7 +153,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildChatEntry(inputWidth),
|
||||
_buildChatControls(inputWidth),
|
||||
const Spacer(),
|
||||
giftAction,
|
||||
SizedBox(width: _compactGap.w),
|
||||
@ -157,6 +173,44 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiAction() {
|
||||
return SizedBox(
|
||||
width: _emojiActionWidth.w,
|
||||
height: 48.w,
|
||||
child: SCDebounceWidget(
|
||||
onTap: () {
|
||||
if (SCRoomUtils.touristCanMsg(context)) {
|
||||
Navigator.push(
|
||||
context,
|
||||
PopRoute(child: const RoomMsgInput(initialShowEmoji: true)),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_icon_room_bottom_emoji.png",
|
||||
width: 34.w,
|
||||
height: 34.w,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _resolveInputWidth({required double maxWidth, required bool showMic}) {
|
||||
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
||||
final actionWidth =
|
||||
_emojiActionWidth.w +
|
||||
_chatEmojiGap.w +
|
||||
_giftActionWidth.w +
|
||||
_circleActionWidth.w * (showMic ? 3 : 2) +
|
||||
(showMic ? 16.w : _compactGap.w * 2);
|
||||
final availableWidth = contentWidth - actionWidth;
|
||||
return availableWidth.clamp(82.w, 112.w).toDouble();
|
||||
}
|
||||
|
||||
double _resolveGiftCenterX({
|
||||
required double maxWidth,
|
||||
required double inputWidth,
|
||||
@ -165,17 +219,28 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
||||
if (showMic) {
|
||||
final occupiedWidth =
|
||||
inputWidth + _giftActionWidth.w + _circleActionWidth.w * 3;
|
||||
inputWidth +
|
||||
_chatEmojiGap.w +
|
||||
_emojiActionWidth.w +
|
||||
_giftActionWidth.w +
|
||||
_circleActionWidth.w * 3;
|
||||
final gapCount = 4;
|
||||
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
|
||||
0.0,
|
||||
double.infinity,
|
||||
);
|
||||
return _horizontalPadding.w + inputWidth + gap + (_giftActionWidth.w / 2);
|
||||
return _horizontalPadding.w +
|
||||
inputWidth +
|
||||
_chatEmojiGap.w +
|
||||
_emojiActionWidth.w +
|
||||
gap +
|
||||
(_giftActionWidth.w / 2);
|
||||
}
|
||||
|
||||
final fixedWidth =
|
||||
inputWidth +
|
||||
_chatEmojiGap.w +
|
||||
_emojiActionWidth.w +
|
||||
_giftActionWidth.w +
|
||||
_circleActionWidth.w * 2 +
|
||||
_compactGap.w * 2;
|
||||
|
||||
67
lib/ui_kit/widgets/room/room_emoji_asset_image.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoomEmojiAssetImage extends StatefulWidget {
|
||||
const RoomEmojiAssetImage({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.fit = BoxFit.contain,
|
||||
});
|
||||
|
||||
final String asset;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
State<RoomEmojiAssetImage> createState() => _RoomEmojiAssetImageState();
|
||||
}
|
||||
|
||||
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late AssetImage _imageProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_precache();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.asset != widget.asset) {
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
_precache();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return RepaintBoundary(
|
||||
child: Image(
|
||||
image: _imageProvider,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
gaplessPlayback: true,
|
||||
filterQuality: FilterQuality.low,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _precache() {
|
||||
precacheImage(_imageProvider, context);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/config/business_logic_strategy.dart';
|
||||
import 'package:yumi/app/constants/sc_emoji_datas.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
@ -14,21 +12,51 @@ import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
|
||||
///聊天输入框
|
||||
class RoomMsgInput extends StatefulWidget {
|
||||
final String? atTextContent;
|
||||
final bool initialShowEmoji;
|
||||
|
||||
const RoomMsgInput({super.key, this.atTextContent});
|
||||
const RoomMsgInput({
|
||||
super.key,
|
||||
this.atTextContent,
|
||||
this.initialShowEmoji = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RoomMsgInput> createState() => _RoomMsgInputState();
|
||||
}
|
||||
|
||||
class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
static const List<String> _roomEmojiAssets = [
|
||||
"sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_02.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_03.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_04.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_05.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_06.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_07.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_08.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_09.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_10.webp",
|
||||
"sc_images/room/emoji/monkey_1.gif",
|
||||
"sc_images/room/emoji/monkey_2.gif",
|
||||
"sc_images/room/emoji/monkey_3.gif",
|
||||
"sc_images/room/emoji/monkey_4.gif",
|
||||
"sc_images/room/emoji/monkey_5.gif",
|
||||
"sc_images/room/emoji/monkey_6.gif",
|
||||
"sc_images/room/emoji/monkey_7.gif",
|
||||
"sc_images/room/emoji/monkey_8.gif",
|
||||
"sc_images/room/emoji/monkey_9.gif",
|
||||
"sc_images/room/emoji/monkey_10.gif",
|
||||
];
|
||||
|
||||
bool showSend = false;
|
||||
bool showEmoji = false;
|
||||
bool _emojiAssetsPrecached = false;
|
||||
|
||||
final FocusNode msgNode = FocusNode();
|
||||
final TextEditingController controller = TextEditingController();
|
||||
@ -39,6 +67,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
showEmoji = widget.initialShowEmoji;
|
||||
if (widget.atTextContent != null) {
|
||||
controller.value = TextEditingValue(text: widget.atTextContent ?? "");
|
||||
showSend = controller.text.trim().isNotEmpty;
|
||||
@ -52,6 +81,14 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (showEmoji) {
|
||||
_precacheRoomEmojis();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
@ -101,8 +138,8 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
child: Image.asset(
|
||||
showEmoji
|
||||
? _strategy.getSCMessageChatPageChatKeyboardIcon()
|
||||
: _strategy.getSCMessageChatPageEmojiIcon(),
|
||||
color: Colors.white,
|
||||
: "sc_images/room/sc_icon_room_bottom_emoji.png",
|
||||
color: showEmoji ? Colors.white : null,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
@ -135,7 +172,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
widget.atTextContent != null ? TextDirection.ltr : null,
|
||||
specialTextSpanBuilder: AtTextSpanBuilder(),
|
||||
focusNode: msgNode,
|
||||
autofocus: true,
|
||||
autofocus: !widget.initialShowEmoji,
|
||||
textInputAction: TextInputAction.send,
|
||||
onTap: () {
|
||||
if (showEmoji) {
|
||||
@ -210,16 +247,21 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
crossAxisSpacing: 8.w,
|
||||
mainAxisSpacing: 8.w,
|
||||
),
|
||||
itemCount: SCEmojiDatas.smileys.length,
|
||||
itemCount: _roomEmojiAssets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final emoji = SCEmojiDatas.smileys[index];
|
||||
final emojiAsset = _roomEmojiAssets[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
_insertEmoji(emoji);
|
||||
_sendRoomEmoji(emojiAsset);
|
||||
},
|
||||
child: Center(
|
||||
child: ExtendedText(emoji, style: TextStyle(fontSize: sp(15))),
|
||||
child: RoomEmojiAssetImage(
|
||||
key: ValueKey(emojiAsset),
|
||||
asset: emojiAsset,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -237,30 +279,47 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
}
|
||||
|
||||
SCKeybordUtil.hide(context);
|
||||
_precacheRoomEmojis();
|
||||
setState(() {
|
||||
showEmoji = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _insertEmoji(String emoji) {
|
||||
final value = controller.value;
|
||||
final selection = value.selection;
|
||||
final fallbackOffset = value.text.length;
|
||||
final start = selection.isValid ? selection.start : fallbackOffset;
|
||||
final end = selection.isValid ? selection.end : fallbackOffset;
|
||||
final safeStart = start < 0 ? fallbackOffset : start;
|
||||
final safeEnd = end < 0 ? fallbackOffset : end;
|
||||
final nextText = value.text.replaceRange(safeStart, safeEnd, emoji);
|
||||
void _precacheRoomEmojis() {
|
||||
if (_emojiAssetsPrecached) {
|
||||
return;
|
||||
}
|
||||
_emojiAssetsPrecached = true;
|
||||
for (final asset in _roomEmojiAssets) {
|
||||
precacheImage(AssetImage(asset), context);
|
||||
}
|
||||
}
|
||||
|
||||
controller.value = value.copyWith(
|
||||
text: nextText,
|
||||
selection: TextSelection.collapsed(offset: safeStart + emoji.length),
|
||||
composing: TextRange.empty,
|
||||
void _sendRoomEmoji(String emojiAsset) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currenRoom = rtcProvider.currenRoom;
|
||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currenRoom == null || currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final seatIndex = rtcProvider.userOnMaiInIndex(currentUser.id ?? "");
|
||||
final msg = Msg(
|
||||
groupId: currenRoom.roomProfile?.roomProfile?.roomAccount ?? "",
|
||||
role: rtcProvider.currenRoom?.entrants?.roles ?? "",
|
||||
msg: emojiAsset,
|
||||
type: SCRoomMsgType.emoticons,
|
||||
user: currentUser,
|
||||
number: seatIndex,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
showSend = controller.text.trim().isNotEmpty;
|
||||
});
|
||||
if (seatIndex > -1) {
|
||||
rtcProvider.starPlayEmoji(msg);
|
||||
}
|
||||
Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).dispatchMessage(msg, addLocal: seatIndex < 0);
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
|
||||
@ -13,6 +13,8 @@ class RoomSeatWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
static const int _defaultVoiceRoomSeatCount = 10;
|
||||
|
||||
int _lastSeatCount = 0;
|
||||
|
||||
int _normalizeSeatCount(int? seatCount) {
|
||||
@ -40,19 +42,40 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
if (snapshot.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) {
|
||||
return _lastSeatCount;
|
||||
}
|
||||
if (snapshot.hasCurrentRoom && seatCount == 0) {
|
||||
return _lastSeatCount > 0 ? _lastSeatCount : _defaultVoiceRoomSeatCount;
|
||||
}
|
||||
return seatCount;
|
||||
}
|
||||
|
||||
int _seatCountFromMicIndexes(Iterable<num> micIndexes) {
|
||||
var maxIndex = -1;
|
||||
var count = 0;
|
||||
for (final micIndex in micIndexes) {
|
||||
count += 1;
|
||||
final resolvedIndex = micIndex.toInt();
|
||||
if (resolvedIndex > maxIndex) {
|
||||
maxIndex = resolvedIndex;
|
||||
}
|
||||
}
|
||||
final indexedSeatCount = _normalizeSeatCount(maxIndex + 1);
|
||||
if (indexedSeatCount > 0) {
|
||||
return indexedSeatCount;
|
||||
}
|
||||
return _normalizeSeatCount(count);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RtcProvider, _RoomSeatLayoutSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomSeatLayoutSnapshot(
|
||||
seatCount: provider.roomWheatMap.length,
|
||||
seatCount: _seatCountFromMicIndexes(provider.roomWheatMap.keys),
|
||||
configuredSeatCount:
|
||||
provider.currenRoom?.roomProfile?.roomSetting?.mikeSize
|
||||
?.toInt() ??
|
||||
0,
|
||||
hasCurrentRoom: provider.currenRoom != null,
|
||||
isExitingCurrentVoiceRoomSession:
|
||||
provider.isExitingCurrentVoiceRoomSession,
|
||||
),
|
||||
@ -211,11 +234,13 @@ class _RoomSeatLayoutSnapshot {
|
||||
const _RoomSeatLayoutSnapshot({
|
||||
required this.seatCount,
|
||||
required this.configuredSeatCount,
|
||||
required this.hasCurrentRoom,
|
||||
required this.isExitingCurrentVoiceRoomSession,
|
||||
});
|
||||
|
||||
final int seatCount;
|
||||
final int configuredSeatCount;
|
||||
final bool hasCurrentRoom;
|
||||
final bool isExitingCurrentVoiceRoomSession;
|
||||
|
||||
@override
|
||||
@ -226,6 +251,7 @@ class _RoomSeatLayoutSnapshot {
|
||||
return other is _RoomSeatLayoutSnapshot &&
|
||||
other.seatCount == seatCount &&
|
||||
other.configuredSeatCount == configuredSeatCount &&
|
||||
other.hasCurrentRoom == hasCurrentRoom &&
|
||||
other.isExitingCurrentVoiceRoomSession ==
|
||||
isExitingCurrentVoiceRoomSession;
|
||||
}
|
||||
@ -234,6 +260,7 @@ class _RoomSeatLayoutSnapshot {
|
||||
int get hashCode => Object.hash(
|
||||
seatCount,
|
||||
configuredSeatCount,
|
||||
hasCurrentRoom,
|
||||
isExitingCurrentVoiceRoomSession,
|
||||
);
|
||||
}
|
||||
|
||||
@ -161,6 +161,7 @@ flutter:
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/room/
|
||||
- sc_images/room/emoji/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/anim/
|
||||
- sc_images/room/anim/gift/
|
||||
@ -170,4 +171,5 @@ flutter:
|
||||
- sc_images/msg/
|
||||
- sc_images/level/
|
||||
- sc_images/coupon/
|
||||
- sc_images/vip/
|
||||
- fonts/
|
||||
|
||||
BIN
sc_images/general/sc_icon_wallet_coin.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
sc_images/room/emoji/fluent_emoji_01.gif
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
sc_images/room/emoji/fluent_emoji_01.webp
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
sc_images/room/emoji/fluent_emoji_02.gif
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
sc_images/room/emoji/fluent_emoji_02.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
sc_images/room/emoji/fluent_emoji_03.gif
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
sc_images/room/emoji/fluent_emoji_03.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
sc_images/room/emoji/fluent_emoji_04.gif
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
sc_images/room/emoji/fluent_emoji_04.webp
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
sc_images/room/emoji/fluent_emoji_05.gif
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
sc_images/room/emoji/fluent_emoji_05.webp
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
sc_images/room/emoji/fluent_emoji_06.gif
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
sc_images/room/emoji/fluent_emoji_06.webp
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
sc_images/room/emoji/fluent_emoji_07.gif
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
sc_images/room/emoji/fluent_emoji_07.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
sc_images/room/emoji/fluent_emoji_08.gif
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
sc_images/room/emoji/fluent_emoji_08.webp
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
sc_images/room/emoji/fluent_emoji_09.gif
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
sc_images/room/emoji/fluent_emoji_09.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
sc_images/room/emoji/fluent_emoji_10.gif
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
sc_images/room/emoji/fluent_emoji_10.webp
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
sc_images/room/emoji/monkey_1.gif
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
sc_images/room/emoji/monkey_10.gif
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
sc_images/room/emoji/monkey_2.gif
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
sc_images/room/emoji/monkey_3.gif
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
sc_images/room/emoji/monkey_4.gif
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
sc_images/room/emoji/monkey_5.gif
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
sc_images/room/emoji/monkey_6.gif
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
sc_images/room/emoji/monkey_7.gif
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
sc_images/room/emoji/monkey_8.gif
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
sc_images/room/emoji/monkey_9.gif
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
sc_images/room/sc_icon_room_bottom_emoji.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
sc_images/vip/sc_vip_activate_button_bg.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
sc_images/vip/sc_vip_badge_1.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
sc_images/vip/sc_vip_badge_2.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
sc_images/vip/sc_vip_badge_3.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
sc_images/vip/sc_vip_badge_4.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
sc_images/vip/sc_vip_badge_5.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
sc_images/vip/sc_vip_card_bg_1.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
sc_images/vip/sc_vip_card_bg_2.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
sc_images/vip/sc_vip_card_bg_3.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
sc_images/vip/sc_vip_card_bg_4.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
sc_images/vip/sc_vip_card_bg_5.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
sc_images/vip/sc_vip_card_ornament_1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
sc_images/vip/sc_vip_card_ornament_2.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
sc_images/vip/sc_vip_card_ornament_3.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
sc_images/vip/sc_vip_card_ornament_4.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
sc_images/vip/sc_vip_card_ornament_5.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
sc_images/vip/sc_vip_entry_bg.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
sc_images/vip/sc_vip_letter_i.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
sc_images/vip/sc_vip_letter_p.png
Normal file
|
After Width: | Height: | Size: 792 B |
BIN
sc_images/vip/sc_vip_letter_v.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
sc_images/vip/sc_vip_number_1.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
sc_images/vip/sc_vip_number_1_filled.png
Normal file
|
After Width: | Height: | Size: 1004 B |
BIN
sc_images/vip/sc_vip_number_2.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sc_images/vip/sc_vip_number_2_filled.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
sc_images/vip/sc_vip_number_3.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
sc_images/vip/sc_vip_number_3_filled.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
sc_images/vip/sc_vip_number_4.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
sc_images/vip/sc_vip_number_4_filled.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
sc_images/vip/sc_vip_number_5.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
sc_images/vip/sc_vip_number_5_filled.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
sc_images/vip/sc_vip_page_bg.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
sc_images/vip/sc_vip_question.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
sc_images/vip/sc_vip_selected_line.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
sc_images/vip/sc_vip_title_left.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
sc_images/vip/sc_vip_title_right.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
sc_images/vip/sc_vip_top_border_1.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
sc_images/vip/sc_vip_top_border_2.png
Normal file
|
After Width: | Height: | Size: 12 KiB |