929 lines
28 KiB
Dart
929 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
|
import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart';
|
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
|
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
|
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
|
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
|
|
|
enum _CpGuideTab { note, rights }
|
|
|
|
enum _CpRightsCategory { cp, brother, sisters }
|
|
|
|
class CpRightsGuidePage extends StatefulWidget {
|
|
const CpRightsGuidePage({super.key, this.cpUserId});
|
|
|
|
final String? cpUserId;
|
|
|
|
@override
|
|
State<CpRightsGuidePage> createState() => _CpRightsGuidePageState();
|
|
}
|
|
|
|
class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
|
|
static const String _assetBase = "sc_images/room/cp_rights";
|
|
static const Color _textColor = Color(0xff543356);
|
|
static const Color _accentColor = Color(0xffff2a70);
|
|
static const String _effectSuppressReason = "cp_rights_guide_page";
|
|
static const String _roomRocketPagLaunchDialogTag =
|
|
"showRoomRocketPagLaunchDialog";
|
|
|
|
_CpGuideTab _tab = _CpGuideTab.note;
|
|
_CpRightsCategory _rightsCategory = _CpRightsCategory.cp;
|
|
RtcProvider? _suppressedRtcProvider;
|
|
bool _roomEffectsSuppressed = false;
|
|
bool _restoreRoomVisualEffects = false;
|
|
final Map<_CpRightsCategory, List<_CpRightsLevel>> _rightsRowsByCategory = {};
|
|
final Set<_CpRightsCategory> _loadingRightsCategories = {};
|
|
final Set<_CpRightsCategory> _loadedRightsCategories = {};
|
|
final Set<_CpRightsCategory> _failedRightsCategories = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_suppressRoomEffects();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_releaseRoomEffects();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xffffe7eb),
|
|
body: Stack(
|
|
children: [
|
|
Positioned.fill(child: _buildBackground()),
|
|
SafeArea(
|
|
top: true,
|
|
bottom: false,
|
|
child: Column(
|
|
children: [
|
|
_buildNavigationBar(),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: EdgeInsets.only(bottom: 28.w),
|
|
child: Column(
|
|
children: [
|
|
SizedBox(height: 15.w),
|
|
_buildTopTabs(),
|
|
SizedBox(height: 14.w),
|
|
if (_tab == _CpGuideTab.note)
|
|
_buildNoteContent()
|
|
else
|
|
_buildRightsContent(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBackground() {
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Color(0xffffd8e6), Color(0xffffe7eb)],
|
|
),
|
|
),
|
|
child: Opacity(
|
|
opacity: 0.6,
|
|
child: Image.asset(
|
|
"$_assetBase/sc_cp_rights_bg.png",
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNavigationBar() {
|
|
return SizedBox(
|
|
height: 44.w,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
PositionedDirectional(
|
|
start: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => Navigator.of(context).maybePop(),
|
|
child: SizedBox(
|
|
width: 44.w,
|
|
child: Center(
|
|
child: Transform.flip(
|
|
flipX: SCGlobalConfig.lang == "ar",
|
|
child: Icon(
|
|
Icons.arrow_back_ios_new,
|
|
color: Colors.black,
|
|
size: 18.w,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 54.w),
|
|
child: Text(
|
|
"Gifts For Close Friends",
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.center,
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 18.sp,
|
|
height: 1,
|
|
fontWeight: FontWeight.w500,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTopTabs() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_buildMainTab(label: "Note", tab: _CpGuideTab.note),
|
|
SizedBox(width: 44.w),
|
|
_buildMainTab(label: "Rights", tab: _CpGuideTab.rights),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMainTab({required String label, required _CpGuideTab tab}) {
|
|
final selected = _tab == tab;
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
if (_tab == tab) {
|
|
return;
|
|
}
|
|
setState(() => _tab = tab);
|
|
if (tab == _CpGuideTab.rights) {
|
|
_ensureRightsConfigLoaded(_rightsCategory);
|
|
}
|
|
},
|
|
child: SizedBox(
|
|
width: 116.w,
|
|
height: 36.w,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
selected
|
|
? "$_assetBase/sc_cp_rights_tab_selected.png"
|
|
: "$_assetBase/sc_cp_rights_tab_unselected.png",
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: selected ? Colors.white : _textColor,
|
|
fontSize: 14.sp,
|
|
height: 1,
|
|
fontWeight: FontWeight.w400,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNoteContent() {
|
|
return Column(
|
|
children: [
|
|
_CpOrnatePanel(
|
|
assetBase: _assetBase,
|
|
title: "What is a close friendship?",
|
|
height: 223.w,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(20.w, 67.w, 20.w, 18.w),
|
|
child: Text(
|
|
"The \"Best Friends\" feature is a brand-new interactive way to celebrate close relationships between users. By sending gifts to someone and requesting to become best friends, you can form a best friend connection once they accept.\n"
|
|
"'CP' connections can only be formed between a man and a woman, while \"brother\" and \"sister\" connections have no gender restrictions.",
|
|
textAlign: TextAlign.left,
|
|
textScaler: TextScaler.noScaling,
|
|
style: _bodyStyle(),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 12.w),
|
|
_CpOrnatePanel(
|
|
assetBase: _assetBase,
|
|
title: "How to Form Relationships?",
|
|
height: 940.w,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(20.w, 67.w, 20.w, 16.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_bodyText(
|
|
"Request for a Gift Panel in a Group Chat Room",
|
|
color: _accentColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
SizedBox(height: 5.w),
|
|
_bodyText(
|
|
"1. In the room, tap [Gift Panel] > select [Best Friends];",
|
|
),
|
|
SizedBox(height: 8.w),
|
|
_noteImage("sc_cp_rights_note_gift_panel.png", 122),
|
|
SizedBox(height: 13.w),
|
|
_bodyText(
|
|
"2. Choose a gift tailored to a specific type of close friend; once you send it, a friend request will be automatically sent;",
|
|
),
|
|
SizedBox(height: 10.w),
|
|
_noteImage("sc_cp_rights_note_relation_types.png", 42),
|
|
SizedBox(height: 13.w),
|
|
_bodyText(
|
|
"3. The other party must accept the match within 24 hours;",
|
|
),
|
|
SizedBox(height: 8.w),
|
|
_noteImage("sc_cp_rights_note_invitation.png", 408),
|
|
SizedBox(height: 9.w),
|
|
_bodyText(
|
|
"*If the other party does not respond within 24 hours of the request being sent, the connection will be automatically declined.",
|
|
color: _accentColor,
|
|
fontSize: 10,
|
|
lineHeight: 1.4,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 12.w),
|
|
_CpOrnatePanel(
|
|
assetBase: _assetBase,
|
|
title: "Intimacy Level",
|
|
height: 174.w,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(20.w, 67.w, 20.w, 16.w),
|
|
child: _bodyText(
|
|
"Intimacy Points are a numerical representation of the level of closeness between two players. 1 Gold Coin equals 1 Intimacy Point, and you can increase your Intimacy Points by sending gifts to your best friend every day.",
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 12.w),
|
|
_CpOrnatePanel(
|
|
assetBase: _assetBase,
|
|
title: "FAQ",
|
|
height: 486.w,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(20.w, 67.w, 20.w, 18.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: const [
|
|
_FaqItem(
|
|
question:
|
|
"Q1: Does giving someone a regular gift increase intimacy?",
|
|
answer: "No increase.",
|
|
),
|
|
_FaqItem(
|
|
question:
|
|
"Q2: Is there a limit to the number of relationships?",
|
|
answer:
|
|
"You can only have one CP, and a maximum of three brothers or sisters. More slots will be added in the future.",
|
|
),
|
|
_FaqItem(
|
|
question: "Q3: Who can I invite to be a CP?",
|
|
answer:
|
|
"As long as neither party has a CP, you can send a CP gift to extend a relationship invitation.",
|
|
),
|
|
_FaqItem(
|
|
question:
|
|
"Q4: If my application is denied, will the Best Friend Gift be refunded?",
|
|
answer:
|
|
"Experience points are awarded the moment the gift is sent. If the request expires or is rejected, the gift will not be returned.",
|
|
),
|
|
_FaqItem(
|
|
question:
|
|
"Q5: Can the termination of the relationship be reversed?",
|
|
answer:
|
|
"Once the relationship is dissolved, all rights and interests will be reclaimed by the platform. The action cannot be undone; the relationship can only be reestablished.",
|
|
isLast: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _noteImage(String name, double height) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(8.w),
|
|
child: Image.asset(
|
|
"$_assetBase/$name",
|
|
width: 310.w,
|
|
height: height.w,
|
|
fit: BoxFit.fill,
|
|
),
|
|
);
|
|
}
|
|
|
|
TextStyle _bodyStyle({
|
|
Color color = _textColor,
|
|
FontWeight fontWeight = FontWeight.w400,
|
|
double fontSize = 12,
|
|
double lineHeight = 1.333,
|
|
}) {
|
|
return TextStyle(
|
|
color: color,
|
|
fontSize: fontSize.sp,
|
|
height: lineHeight,
|
|
fontWeight: fontWeight,
|
|
decoration: TextDecoration.none,
|
|
);
|
|
}
|
|
|
|
Widget _bodyText(
|
|
String value, {
|
|
Color color = _textColor,
|
|
FontWeight fontWeight = FontWeight.w400,
|
|
double fontSize = 12,
|
|
double lineHeight = 1.333,
|
|
}) {
|
|
return Text(
|
|
value,
|
|
textAlign: TextAlign.left,
|
|
textScaler: TextScaler.noScaling,
|
|
style: _bodyStyle(
|
|
color: color,
|
|
fontWeight: fontWeight,
|
|
fontSize: fontSize,
|
|
lineHeight: lineHeight,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRightsContent() {
|
|
final rows = _currentRightsRowsFor(_rightsCategory);
|
|
final isInitialLoading =
|
|
_loadingRightsCategories.contains(_rightsCategory) &&
|
|
!_rightsRowsByCategory.containsKey(_rightsCategory);
|
|
final hasLoadError =
|
|
_failedRightsCategories.contains(_rightsCategory) &&
|
|
!_rightsRowsByCategory.containsKey(_rightsCategory);
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 52.w),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildRightsCategoryTab("CP", _CpRightsCategory.cp),
|
|
_buildRightsCategoryTab("Brother", _CpRightsCategory.brother),
|
|
_buildRightsCategoryTab("Sisters", _CpRightsCategory.sisters),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 13.w),
|
|
if (hasLoadError)
|
|
_buildRightsLoadError(_rightsCategory)
|
|
else
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
_buildRightsTable(rows),
|
|
if (isInitialLoading)
|
|
SizedBox(
|
|
width: 22.w,
|
|
height: 22.w,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.w,
|
|
color: _accentColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRightsCategoryTab(String label, _CpRightsCategory category) {
|
|
final selected = _rightsCategory == category;
|
|
final textStyle = TextStyle(
|
|
color: selected ? _accentColor : _textColor.withValues(alpha: 0.5),
|
|
fontSize: 16.sp,
|
|
height: 1,
|
|
fontWeight: FontWeight.w400,
|
|
decoration: TextDecoration.none,
|
|
);
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
if (_rightsCategory == category) {
|
|
return;
|
|
}
|
|
setState(() => _rightsCategory = category);
|
|
_ensureRightsConfigLoaded(category);
|
|
},
|
|
child: SizedBox(
|
|
height: 26.w,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, textScaler: TextScaler.noScaling, style: textStyle),
|
|
SizedBox(height: 6.w),
|
|
if (selected)
|
|
Container(
|
|
width: _rightsIndicatorWidth(label, textStyle),
|
|
height: 3.w,
|
|
decoration: BoxDecoration(
|
|
color: _accentColor,
|
|
borderRadius: BorderRadius.circular(31.5.w),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRightsLoadError(_CpRightsCategory category) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _ensureRightsConfigLoaded(category, force: true),
|
|
child: Container(
|
|
width: 351.w,
|
|
height: 156.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.3),
|
|
borderRadius: BorderRadius.circular(12.w),
|
|
),
|
|
alignment: Alignment.center,
|
|
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
|
child: Text(
|
|
"Failed to load rights. Tap to retry.",
|
|
textAlign: TextAlign.center,
|
|
textScaler: TextScaler.noScaling,
|
|
style: _bodyStyle(color: _accentColor, lineHeight: 1.3),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<_CpRightsLevel> _rightsRowsFor(_CpRightsCategory category) {
|
|
switch (category) {
|
|
case _CpRightsCategory.cp:
|
|
return const [
|
|
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
|
];
|
|
case _CpRightsCategory.brother:
|
|
return const [
|
|
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
|
];
|
|
case _CpRightsCategory.sisters:
|
|
return const [
|
|
_CpRightsLevel(level: "Lv.1", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.2", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.3", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.4", intimacy: "--"),
|
|
_CpRightsLevel(level: "Lv.5", intimacy: "--"),
|
|
];
|
|
}
|
|
}
|
|
|
|
List<_CpRightsLevel> _currentRightsRowsFor(_CpRightsCategory category) {
|
|
return _rightsRowsByCategory[category] ?? _rightsRowsFor(category);
|
|
}
|
|
|
|
Future<void> _ensureRightsConfigLoaded(
|
|
_CpRightsCategory category, {
|
|
bool force = false,
|
|
}) async {
|
|
if (!force &&
|
|
(_loadedRightsCategories.contains(category) ||
|
|
_loadingRightsCategories.contains(category))) {
|
|
return;
|
|
}
|
|
final relationType = _relationTypeForRightsCategory(category);
|
|
setState(() {
|
|
_loadingRightsCategories.add(category);
|
|
_failedRightsCategories.remove(category);
|
|
});
|
|
_rightsDebugLog(
|
|
'load start relationType=$relationType cpUserId=${widget.cpUserId}',
|
|
);
|
|
try {
|
|
final config = await SCAccountRepository().cpRightsConfig(
|
|
relationType: relationType,
|
|
cpUserId: widget.cpUserId,
|
|
);
|
|
final rows = _rightsRowsFromConfig(config, category);
|
|
if (rows.isEmpty) {
|
|
throw StateError('empty CP rights levels');
|
|
}
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_rightsRowsByCategory[category] = rows;
|
|
_loadedRightsCategories.add(category);
|
|
_failedRightsCategories.remove(category);
|
|
_loadingRightsCategories.remove(category);
|
|
});
|
|
_rightsDebugLog(
|
|
'load success relationType=$relationType levels=${rows.length}',
|
|
);
|
|
} catch (error) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_loadingRightsCategories.remove(category);
|
|
_failedRightsCategories.add(category);
|
|
});
|
|
_rightsDebugLog('load failed relationType=$relationType error=$error');
|
|
}
|
|
}
|
|
|
|
List<_CpRightsLevel> _rightsRowsFromConfig(
|
|
SCCpRightsConfigRes config,
|
|
_CpRightsCategory category,
|
|
) {
|
|
final rows = <_CpRightsLevel>[];
|
|
for (final level in config.levels) {
|
|
final levelNumber = level.level;
|
|
final levelName = level.levelName?.trim();
|
|
final requiredValue = level.requiredIntimacyValue ?? 0;
|
|
rows.add(
|
|
_CpRightsLevel(
|
|
level:
|
|
levelName?.isNotEmpty == true
|
|
? levelName!
|
|
: (levelNumber != null
|
|
? "Lv.$levelNumber"
|
|
: "Lv.${rows.length + 1}"),
|
|
intimacy: _formatRightsIntimacyValue(requiredValue),
|
|
),
|
|
);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
String _relationTypeForRightsCategory(_CpRightsCategory category) {
|
|
switch (category) {
|
|
case _CpRightsCategory.cp:
|
|
return "CP";
|
|
case _CpRightsCategory.brother:
|
|
return "BROTHER";
|
|
case _CpRightsCategory.sisters:
|
|
return "SISTERS";
|
|
}
|
|
}
|
|
|
|
String _formatRightsIntimacyValue(int value) {
|
|
final text = value.abs().toString();
|
|
final buffer = StringBuffer();
|
|
for (var index = 0; index < text.length; index++) {
|
|
if (index > 0 && (text.length - index) % 3 == 0) {
|
|
buffer.write(",");
|
|
}
|
|
buffer.write(text[index]);
|
|
}
|
|
return value < 0 ? "-$buffer" : buffer.toString();
|
|
}
|
|
|
|
void _rightsDebugLog(String message) {
|
|
if (kDebugMode) {
|
|
debugPrint("[CP][Rights] $message");
|
|
}
|
|
}
|
|
|
|
Widget _buildRightsTable(List<_CpRightsLevel> rows) {
|
|
const tableWidth = 351.0;
|
|
const columnWidth = tableWidth / 2;
|
|
const headerHeight = 39.0;
|
|
const rowHeight = 77.0;
|
|
final tableHeight = headerHeight + rows.length * rowHeight;
|
|
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(12.w),
|
|
child: Container(
|
|
width: tableWidth.w,
|
|
height: tableHeight.w,
|
|
color: Colors.white.withValues(alpha: 0.3),
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: 0,
|
|
bottom: 0,
|
|
left: columnWidth.w,
|
|
width: columnWidth.w,
|
|
child: ColoredBox(color: Colors.white.withValues(alpha: 0.24)),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
top: headerHeight.w,
|
|
child: _tableLine(width: tableWidth),
|
|
),
|
|
Positioned.fill(
|
|
child: Column(
|
|
children: [
|
|
SizedBox(
|
|
height: headerHeight.w,
|
|
child: Row(
|
|
children: [
|
|
_tableHeader("Level", columnWidth),
|
|
_tableHeader("Intimacy Level", columnWidth),
|
|
],
|
|
),
|
|
),
|
|
for (final row in rows) _buildRightsLevelRow(row),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tableHeader(String value, double width) {
|
|
return SizedBox(
|
|
width: width.w,
|
|
child: Center(
|
|
child: Text(
|
|
value,
|
|
textScaler: TextScaler.noScaling,
|
|
style: _bodyStyle(fontSize: 12, lineHeight: 1.2),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRightsLevelRow(_CpRightsLevel row) {
|
|
const tableWidth = 351.0;
|
|
const columnWidth = tableWidth / 2;
|
|
const rowHeight = 77.0;
|
|
return SizedBox(
|
|
height: rowHeight.w,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Row(
|
|
children: [
|
|
_spanCell(row.level, width: columnWidth, height: rowHeight),
|
|
_spanCell(row.intimacy, width: columnWidth, height: rowHeight),
|
|
],
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: _tableLine(width: tableWidth),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _spanCell(
|
|
String value, {
|
|
required double width,
|
|
required double height,
|
|
}) {
|
|
return SizedBox(
|
|
width: width.w,
|
|
height: height.w,
|
|
child: Center(
|
|
child: Text(
|
|
value,
|
|
textScaler: TextScaler.noScaling,
|
|
style: _bodyStyle(fontSize: 12, lineHeight: 1.2),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tableLine({required double width}) {
|
|
return Container(width: width.w, height: 1.w, color: Colors.white);
|
|
}
|
|
|
|
double _rightsIndicatorWidth(String label, TextStyle style) {
|
|
final painter = TextPainter(
|
|
text: TextSpan(text: label, style: style),
|
|
textDirection: Directionality.of(context),
|
|
textScaler: TextScaler.noScaling,
|
|
maxLines: 1,
|
|
)..layout();
|
|
return painter.width.clamp(16.w, 72.w);
|
|
}
|
|
|
|
void _suppressRoomEffects() {
|
|
if (_roomEffectsSuppressed) {
|
|
return;
|
|
}
|
|
_roomEffectsSuppressed = true;
|
|
try {
|
|
final rtcProvider = context.read<RtcProvider>();
|
|
_suppressedRtcProvider = rtcProvider;
|
|
_restoreRoomVisualEffects = rtcProvider.roomVisualEffectsEnabled;
|
|
rtcProvider.setRoomVisualEffectsEnabled(false);
|
|
} catch (_) {}
|
|
OverlayManager().beginSuppressFloatingScreens(
|
|
reason: _effectSuppressReason,
|
|
);
|
|
SCRoomEffectScheduler().clearDeferredTasks(reason: _effectSuppressReason);
|
|
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
|
SCGiftVapSvgaManager().stopPlayback();
|
|
SCGiftVapSvgaManager().pauseAnim();
|
|
}
|
|
|
|
void _releaseRoomEffects() {
|
|
if (!_roomEffectsSuppressed) {
|
|
return;
|
|
}
|
|
_roomEffectsSuppressed = false;
|
|
SCGiftVapSvgaManager().stopPlayback();
|
|
OverlayManager().endSuppressFloatingScreens(reason: _effectSuppressReason);
|
|
final rtcProvider = _suppressedRtcProvider;
|
|
if (rtcProvider != null && _restoreRoomVisualEffects) {
|
|
rtcProvider.setRoomVisualEffectsEnabled(true);
|
|
}
|
|
SCGiftVapSvgaManager().resumeAnim();
|
|
_suppressedRtcProvider = null;
|
|
}
|
|
}
|
|
|
|
class _CpOrnatePanel extends StatelessWidget {
|
|
const _CpOrnatePanel({
|
|
required this.assetBase,
|
|
required this.title,
|
|
required this.height,
|
|
required this.child,
|
|
});
|
|
|
|
static const Color _textColor = Color(0xff543356);
|
|
|
|
final String assetBase;
|
|
final String title;
|
|
final double height;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final middleHeight = (height - 65.w - 73.w).clamp(1.0, double.infinity);
|
|
return SizedBox(
|
|
width: 350.w,
|
|
height: height,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Column(
|
|
children: [
|
|
Image.asset(
|
|
"$assetBase/sc_cp_rights_panel_top.png",
|
|
width: 350.w,
|
|
height: 65.w,
|
|
fit: BoxFit.fill,
|
|
),
|
|
Image.asset(
|
|
"$assetBase/sc_cp_rights_panel_mid.png",
|
|
width: 350.w,
|
|
height: middleHeight,
|
|
fit: BoxFit.fill,
|
|
),
|
|
Image.asset(
|
|
"$assetBase/sc_cp_rights_panel_bottom.png",
|
|
width: 350.w,
|
|
height: 73.w,
|
|
fit: BoxFit.fill,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 24.w,
|
|
left: 0,
|
|
right: 0,
|
|
child: Text(
|
|
title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.center,
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: _textColor,
|
|
fontSize: 16.sp,
|
|
height: 1,
|
|
fontWeight: FontWeight.w400,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 30.w,
|
|
right: 30.w,
|
|
top: 44.w,
|
|
child: Image.asset(
|
|
"$assetBase/sc_cp_rights_panel_divider.png",
|
|
height: 20.w,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
Positioned.fill(child: child),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FaqItem extends StatelessWidget {
|
|
const _FaqItem({
|
|
required this.question,
|
|
required this.answer,
|
|
this.isLast = false,
|
|
});
|
|
|
|
static const Color _textColor = Color(0xff543356);
|
|
static const Color _accentColor = Color(0xffff2a70);
|
|
|
|
final String question;
|
|
final String answer;
|
|
final bool isLast;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: isLast ? 0 : 8.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
question,
|
|
textAlign: TextAlign.left,
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: _accentColor,
|
|
fontSize: 12.sp,
|
|
height: 1.333,
|
|
fontWeight: FontWeight.w500,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
SizedBox(height: 2.w),
|
|
Text(
|
|
answer,
|
|
textAlign: TextAlign.left,
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: _textColor,
|
|
fontSize: 12.sp,
|
|
height: 1.333,
|
|
fontWeight: FontWeight.w400,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CpRightsLevel {
|
|
const _CpRightsLevel({required this.level, required this.intimacy});
|
|
|
|
final String level;
|
|
final String intimacy;
|
|
}
|