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 createState() => _CpRightsGuidePageState(); } class _CpRightsGuidePageState extends State { 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 _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(); _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; }