1.0.7 版本需求

This commit is contained in:
zhangxianbiao 2026-07-15 10:21:00 +08:00
parent ee53279985
commit 7846b992b5
34 changed files with 1271 additions and 54 deletions

View File

@ -325,6 +325,7 @@
"inRoom": "في الغرفة",
"inRocket": "في الصاروخ",
"roomRocketHelpTips": "1. إرسال الهدايا في الغرفة يزيد من طاقة الصاروخ. *هدية واحدة من العملات الذهبية = نقطة طاقة صاروخية واحدة؛ الهدايا المحظوظة تزيد من طاقة الصاروخ بنسبة 4% من قيمة العملة الذهبية للهدية.\n2. بعد اكتمال شحن الطاقة، يمكنك إطلاق الصاروخ. سيتم توزيع المكافآت تلقائيًا بعد إطلاق الصاروخ.\n3. تقدم فئات الصواريخ المختلفة مكافآت مختلفة.\n4. عند إطلاق الصاروخ، يمكن لجميع المستخدمين في الغرفة المطالبة بمكافأة الصاروخ.\n5. سيتم تحديث طاقة الصاروخ في الساعة 0:00 كل يوم.",
"roomRocketNoteTitle": "ملاحظة",
"roomRocketRecordAction": "السجل",
"roomRocketRewardTop1": "الأول",
"roomRocketRewardSetOff": "الإطلاق",
@ -333,6 +334,7 @@
"roomRocketRecordRoom": "الغرفة",
"roomRocketRecordReward": "المكافأة",
"roomRocketRecordLast35Days": "عرض سجلات آخر 35 يومًا فقط",
"roomRocketRecordEmpty": "لم يتم العثور على سجلات مكافآت",
"searchCouponHint": "ابحث عن كوبون",
"search": "بحث",
"checkInSuccessful": "تسجيل الوصول ناجح",

View File

@ -389,6 +389,7 @@
"get": "পান করুন",
"inRocket": "রকেটে",
"roomRocketHelpTips": "1. রুমে উপহার পাঠান রকেট শক্তি বাড়ায়। *1 সোনা কয়েন উপহার = 1 রকেট শক্তি পয়েন্ট; ভাগ্যশালী উপহার রকেট শক্তি উপহারের সোনা কয়েন মানের 4% পরিমাণে বাড়ায়।\n2. রকেট শক্তি সম্পূর্ণভাবে পূর্ণ হলে, রুম রকেট চালু করতে পারে। চালু করার পর পুরস্কার স্বয়ংক্রিয়ভাবে বিতরণ করা হবে।\n3. বিভিন্ন রকেট স্তর বিভিন্ন পুরস্কার প্রদান করে।\n4. রকেট চালু হলে, রুমের সমস্ত ব্যবহারকারী রকেট পুরস্কার দাবি করতে পারে।5. রকেট শক্তি প্রতিদিন 00:00 এ শূন্য হয়ে যায়।",
"roomRocketNoteTitle": "দ্রষ্টব্য",
"roomRocketRecordAction": "রেকর্ড",
"roomRocketRewardTop1": "শীর্ষ 1",
"roomRocketRewardSetOff": "চালু",
@ -397,6 +398,7 @@
"roomRocketRecordRoom": "রুম",
"roomRocketRecordReward": "পুরস্কার",
"roomRocketRecordLast35Days": "শুধু গত ৩৫ দিনের রেকর্ড দেখান",
"roomRocketRecordEmpty": "কোনো পুরস্কারের রেকর্ড পাওয়া যায়নি",
"couponRecord": "কুপন ব্যবহার রেকর্ড",
"inRoom": "রুমে",
"searchCouponHint": "কুপন সার্চ করুন",

View File

@ -343,6 +343,7 @@
"get": "Get",
"inRocket": "In the rocket",
"roomRocketHelpTips": "1. Sending gifts in the room increases rocket energy. *1 gold coin gift = 1 rocket energy point; lucky gifts increase rocket energy by 4% of the gift's gold coin value.\n2. Once the rocket energy is fully charged, the room can launch the rocket. Rewards will be automatically distributed after launch.\n3. Different rocket levels offer different rewards.\n4. When the rocket launches, all users in the room can claim the rocket reward.5. Rocket energy is reset at 00:00 every day.",
"roomRocketNoteTitle": "Note",
"roomRocketRecordAction": "Record",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "Set off",
@ -351,6 +352,7 @@
"roomRocketRecordRoom": "Room",
"roomRocketRecordReward": "Reward",
"roomRocketRecordLast35Days": "Show only records from the last 35 days",
"roomRocketRecordEmpty": "No reward records found",
"couponRecord": "Coupon usage record",
"inRoom": "In Room",
"searchCouponHint": "Search Coupon",

View File

@ -343,6 +343,7 @@
"get": "Kunin",
"inRocket": "Sa rocket",
"roomRocketHelpTips": "1. Ang pagpapadala ng mga regalo sa silid ay nagpapataas ng enerhiya ng rocket. *1 regalong gintong barya = 1 rocket energy point; pinapataas ng mga masuwerteng regalo ang rocket energy ng 4% ng halaga ng gintong barya ng regalo.\n2. Kapag ang enerhiya ng rocket ay ganap na na-charge, maaaring ilunsad ng silid ang rocket. Awtomatikong ipapamahagi ang mga reward pagkatapos ilunsad.\n3. Ang iba't ibang antas ng rocket ay nag-aalok ng iba't ibang mga gantimpala.\n4. Kapag naglunsad ang rocket, lahat ng user sa kuwarto ay maaaring mag-claim ng rocket reward.5. Nire-reset ang rocket energy sa 00:00 araw-araw.",
"roomRocketNoteTitle": "Paalala",
"roomRocketRecordAction": "Record",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "Set off",
@ -351,6 +352,7 @@
"roomRocketRecordRoom": "Room",
"roomRocketRecordReward": "Reward",
"roomRocketRecordLast35Days": "Ipakita lang ang records sa nakaraang 35 araw",
"roomRocketRecordEmpty": "Walang nakitang reward records",
"couponRecord": "Talaan ng paggamit ng kupon",
"inRoom": "Sa Kwarto",
"searchCouponHint": "Search Coupon",

View File

@ -343,6 +343,7 @@
"get": "पाना",
"inRocket": "रॉकेट में",
"roomRocketHelpTips": "1. कमरे में उपहार भेजने से रॉकेट ऊर्जा बढ़ती है। *1 सोने का सिक्का उपहार = 1 रॉकेट ऊर्जा बिंदु; भाग्यशाली उपहार रॉकेट ऊर्जा को उपहार के सोने के सिक्के के मूल्य से 4% तक बढ़ा देते हैं।\n2. एक बार रॉकेट ऊर्जा पूरी तरह चार्ज हो जाने पर, कक्ष रॉकेट लॉन्च कर सकता है। लॉन्च के बाद पुरस्कार स्वचालित रूप से वितरित किए जाएंगे।\n3. विभिन्न रॉकेट स्तर अलग-अलग पुरस्कार प्रदान करते हैं।\n4. जब रॉकेट लॉन्च होता है, तो कमरे में मौजूद सभी उपयोगकर्ता रॉकेट इनाम का दावा कर सकते हैं।5. रॉकेट ऊर्जा हर दिन 00:00 बजे रीसेट की जाती है।",
"roomRocketNoteTitle": "नोट",
"roomRocketRecordAction": "रिकॉर्ड",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "लॉन्च",
@ -351,6 +352,7 @@
"roomRocketRecordRoom": "कमरा",
"roomRocketRecordReward": "इनाम",
"roomRocketRecordLast35Days": "केवल पिछले 35 दिनों के रिकॉर्ड दिखाएं",
"roomRocketRecordEmpty": "कोई इनाम रिकॉर्ड नहीं मिला",
"couponRecord": "कूपन उपयोग रिकॉर्ड",
"inRoom": "कमरे में",
"searchCouponHint": "कूपन खोजें",

View File

@ -343,6 +343,7 @@
"get": "Mendapatkan",
"inRocket": "Di dalam roket",
"roomRocketHelpTips": "1. Mengirim hadiah di dalam ruangan meningkatkan energi roket. *1 hadiah koin emas = 1 poin energi roket; hadiah keberuntungan meningkatkan energi roket sebesar 4% dari nilai koin emas hadiah.\n2. Setelah energi roket terisi penuh, ruangan dapat meluncurkan roket. Hadiah akan didistribusikan secara otomatis setelah peluncuran.\n3. Level roket yang berbeda menawarkan hadiah yang berbeda.\n4. Saat roket diluncurkan, semua pengguna di ruangan dapat mengklaim hadiah roket.5. Energi roket disetel ulang pada pukul 00:00 setiap hari.",
"roomRocketNoteTitle": "Catatan",
"roomRocketRecordAction": "Catatan",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "Diluncurkan",
@ -351,6 +352,7 @@
"roomRocketRecordRoom": "Ruangan",
"roomRocketRecordReward": "Hadiah",
"roomRocketRecordLast35Days": "Hanya tampilkan catatan 35 hari terakhir",
"roomRocketRecordEmpty": "Tidak ada catatan hadiah",
"couponRecord": "Catatan penggunaan kupon",
"inRoom": "Di Kamar",
"searchCouponHint": "Cari Kupon",

View File

@ -359,6 +359,7 @@
"get": "Al",
"inRocket": "Rokette",
"roomRocketHelpTips": "1. Odada hediye göndermek roket enerjisini artırır. *1 altın jetton hediyesi = 1 roket enerji puanı; şanslı hediyeler roket enerjisini hediyenin altın jetton değerinin %4'ü kadar artırır.\n2. Roket enerjisi tamamen dolduğunda, oda roketi fırlatabilir. Fırlatmadan sonra ödüller otomatik olarak dağıtılacaktır.\n3. Farklı roket seviyeleri farklı ödüller sunar.\n4. Roket fırlatıldığında, odadaki tüm kullanıcılar roket ödülünü talep edebilir.5. Roket enerjisi her gün 00:00'da sıfırlanır.",
"roomRocketNoteTitle": "Not",
"roomRocketRecordAction": "Kayıt",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "Fırlatan",
@ -367,6 +368,7 @@
"roomRocketRecordRoom": "Oda",
"roomRocketRecordReward": "Ödül",
"roomRocketRecordLast35Days": "Yalnızca son 35 günün kayıtlarını göster",
"roomRocketRecordEmpty": "Ödül kaydı bulunamadı",
"couponRecord": "Kupon Kullanım Kaydı",
"inRoom": "Odada",
"searchCouponHint": "Kupon Ara",

View File

@ -343,6 +343,7 @@
"get": "حاصل کریں۔",
"inRocket": "راکٹ میں",
"roomRocketHelpTips": "1. کمرے میں تحائف بھیجنے سے راکٹ توانائی میں اضافہ ہوتا ہے۔ *1 سونے کا سکہ تحفہ = 1 راکٹ انرجی پوائنٹ؛ خوش قسمت تحائف تحفے کے سونے کے سکوں کی قیمت کا 4% راکٹ توانائی میں اضافہ کرتے ہیں۔\n2. راکٹ کی توانائی مکمل طور پر چارج ہونے کے بعد، کمرہ راکٹ کو لانچ کر سکتا ہے۔ لانچ کے بعد انعامات خود بخود تقسیم ہو جائیں گے۔\n3. راکٹ کی مختلف سطحیں مختلف انعامات پیش کرتی ہیں۔\n4. جب راکٹ لانچ ہوتا ہے، کمرے میں موجود تمام صارفین راکٹ کے انعام کا دعوی کر سکتے ہیں۔5۔ راکٹ توانائی ہر روز 00:00 پر دوبارہ ترتیب دی جاتی ہے۔",
"roomRocketNoteTitle": "نوٹ",
"roomRocketRecordAction": "ریکارڈ",
"roomRocketRewardTop1": "Top1",
"roomRocketRewardSetOff": "لانچ",
@ -351,6 +352,7 @@
"roomRocketRecordRoom": "کمرہ",
"roomRocketRecordReward": "انعام",
"roomRocketRecordLast35Days": "صرف گزشتہ 35 دنوں کے ریکارڈ دکھائیں",
"roomRocketRecordEmpty": "انعام کا کوئی ریکارڈ نہیں ملا",
"couponRecord": "کوپن کے استعمال کا ریکارڈ",
"inRoom": "کمرے میں",
"searchCouponHint": "کوپن تلاش کریں۔",

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

56
design-qa.md Normal file
View File

@ -0,0 +1,56 @@
# Rocket Floating Window Design QA
- Source visual truth: Figma nodes `3462:8689` and `3462:4964` in `vaDp5wwn6AOQVlt9zk0f9r`.
- Additional visual truth: Figma node `3462:5730` for the not-won reward state.
- Not-won source screenshot: `design-qa-assets/rocket-reward-empty-figma.png`.
- Implementation screenshots:
- `design-qa-assets/rocket-floating-implementation.png`
- `design-qa-assets/rocket-reward-implementation.png`
- `design-qa-assets/rocket-reward-empty-implementation.png`
- Viewport: 375 × 812 at device pixel ratio 1.
- State: floating banner centered after its entrance animation; reward panel open over a dimmed host surface.
- Integration: implemented as the independent `RoomRocketFloatingPopup`; the original rocket page/widget remains unchanged.
## Full-view comparison evidence
- `design-qa-assets/rocket-floating-full-comparison.png`
- `design-qa-assets/rocket-reward-full-comparison.png`
- `design-qa-assets/rocket-reward-empty-full-comparison.png`
The left side is the Figma reference and the right side is the Flutter capture. The host room surface is intentionally represented by a solid test background on the implementation side because the deliverable is the overlay UI. Full-view evidence verifies the overlay position and dimensions within the 375 × 812 viewport.
## Focused comparison evidence
- `design-qa-assets/rocket-floating-comparison.png`
- `design-qa-assets/rocket-reward-comparison.png`
- `design-qa-assets/rocket-reward-empty-comparison.png`
Focused crops were required because the banner typography, reward-card spacing, small coin labels, and decorative frame details are too small to judge reliably in the full-view comparison.
## Findings
- No actionable P0, P1, or P2 visual differences remain.
- Fonts and typography: sizes, weights, line heights, truncation, and hierarchy match the source. The capture uses Arial as a QA substitute for Source Han Sans SC; the app continues to use its platform system font, consistent with the existing project setup.
- Spacing and layout rhythm: the banner is 355 × 103 and begins at y=115 in the full room viewport. The reward panel is 360 × 576 and begins at x=8, y=118. Reward cards, section headings, rows, and footer loading text align with the Figma coordinates.
- Colors and visual tokens: gold/orange surfaces, border highlights, dim mask, and white text use the original Figma raster assets and match the reference palette.
- Image quality and asset fidelity: the banner frame, GO button, rocket, reward frame, reward cards, gift, coin, close button, and avatar frame use source assets. New large assets were resized to approximately 3× their rendered dimensions to preserve sharpness while reducing decode cost.
- Copy and content: `Reward`, `Congratulations on winning`, `12 users received rewards`, prize values, and loading copy match the reference. Avatar/name/reward records remain UI-preview content pending the later business-data requirements.
- Not-won state: the single `RoomRocketRewardPage` switches on `hasRewards`; the 125 × 126 empty reward artwork and `You didn't win a prize. Better luck next time!` copy match node `3462:5730`. The shared frame, close action, winner rows, and loading footer retain the same geometry as the won state.
## Comparison history
1. Initial capture exposed reward-title and mini-prize text overflows and incomplete image decoding.
- Fix: added constrained scaling for narrow labels and waited for raster decoding before capture.
- Post-fix evidence: both focused comparison images; widget test completes without render exceptions.
2. Section decorations rendered too densely.
- Fix: mirrored and spaced the slash marks to match the Figma headings.
- Post-fix evidence: `design-qa-assets/rocket-reward-comparison.png`.
3. The floating-window rocket crop was outside the visible slot.
- Fix: aligned the source rocket artwork to the top of the 20 × 29 crop.
- Post-fix evidence: `design-qa-assets/rocket-floating-comparison.png`.
## Follow-up polish
- P3: production captures will naturally differ from the QA placeholders because real user avatars, names, and reward records will be supplied by the later functional requirements.
final result: passed

View File

@ -744,6 +744,8 @@ class ATAppLocalizations {
String get roomRocketHelpTips => translate('roomRocketHelpTips');
String get roomRocketNoteTitle => translate('roomRocketNoteTitle');
String get roomRocketRecordAction => translate('roomRocketRecordAction');
String get roomRocketRewardTop1 => translate('roomRocketRewardTop1');
@ -761,6 +763,8 @@ class ATAppLocalizations {
String get roomRocketRecordLast35Days =>
translate('roomRocketRecordLast35Days');
String get roomRocketRecordEmpty => translate('roomRocketRecordEmpty');
String get giveUpIdentity => translate('giveUpIdentity');
String get bdLeader => translate('bdLeader');

View File

@ -0,0 +1,100 @@
class ATRoomRocketRewardRes {
const ATRoomRocketRewardRes({
this.rewardType = '',
this.rewardItemId = '',
this.rewardItemName = '',
this.rewardAmount,
this.cover = '',
this.rocketLevel = 1,
this.isContributor = false,
this.contributionEnergy,
this.contributionRate,
this.message = '',
this.userId = '',
this.userNickname = '',
this.userAvatar = '',
});
final String rewardType;
final String rewardItemId;
final String rewardItemName;
final num? rewardAmount;
final String cover;
final int rocketLevel;
final bool isContributor;
final num? contributionEnergy;
final num? contributionRate;
final String message;
/// `ROCKET_REWARD_USER`
final String userId;
final String userNickname;
final String userAvatar;
bool get isCoin {
final String type = rewardType.trim().toUpperCase();
return type == 'GOLD' || type == 'COIN' || type == 'DIAMOND';
}
String get displayLabel {
if (rewardAmount != null) {
return _formatNumber(rewardAmount!);
}
if (rewardItemName.isNotEmpty) {
return rewardItemName;
}
return rewardType.isEmpty ? 'Reward' : rewardType;
}
factory ATRoomRocketRewardRes.fromJson(dynamic data) {
final Map<String, dynamic> json = _asMap(data);
return ATRoomRocketRewardRes(
rewardType: _readString(json['rewardType']),
rewardItemId: _readString(json['rewardItemId']),
rewardItemName: _readString(json['rewardItemName']),
rewardAmount: _readNum(json['rewardAmount']),
cover: _readString(json['cover']),
rocketLevel: _readInt(json['rocketLevel'], fallback: 1),
isContributor: json['isContributor'] == true,
contributionEnergy: _readNum(json['contributionEnergy']),
contributionRate: _readNum(json['contributionRate']),
message: _readString(json['message']),
userId: _readString(json['userId']),
userNickname: _readString(json['userNickname']),
userAvatar: _readString(json['userAvatar']),
);
}
}
Map<String, dynamic> _asMap(dynamic data) {
if (data is! Map) {
return const <String, dynamic>{};
}
return data.map(
(dynamic key, dynamic value) =>
MapEntry<String, dynamic>(key.toString(), value),
);
}
String _readString(dynamic value) => value?.toString().trim() ?? '';
num? _readNum(dynamic value) {
if (value is num) {
return value;
}
return num.tryParse(_readString(value));
}
int _readInt(dynamic value, {required int fallback}) {
if (value is num) {
return value.toInt();
}
return int.tryParse(_readString(value)) ?? fallback;
}
String _formatNumber(num value) {
if (value is int || value == value.roundToDouble()) {
return value.toInt().toString();
}
return value.toString();
}

View File

@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:aslan/chatvibe_ui/components/at_compontent.dart';
import 'package:aslan/chatvibe_core/utilities/at_room_utils.dart';
import 'package:aslan/chatvibe_data/models/message/at_floating_message.dart';
import 'package:aslan/chatvibe_managers/rtc_manager.dart';
import 'package:aslan/main.dart';
import 'package:provider/provider.dart';
class RoomRocketFloatingPopup extends StatefulWidget {
final ATFloatingMessage message;
final VoidCallback onAnimationCompleted; //
const RoomRocketFloatingPopup({
super.key,
required this.message,
required this.onAnimationCompleted,
});
@override
State<RoomRocketFloatingPopup> createState() =>
_RoomRocketFloatingPopupState();
}
class _RoomRocketFloatingPopupState extends State<RoomRocketFloatingPopup>
with TickerProviderStateMixin {
Debouncer debouncer = Debouncer();
late AnimationController _controller;
late Animation<Offset> _firstAnimation; //
late Animation<Offset> _secondAnimation; //
late AnimationController _swipeController; //
late Animation<Offset> _swipeAnimation; //
bool _firstPhaseCompleted = false;
bool _isSwipeAnimating = false; //
@override
void initState() {
super.initState();
//
_controller = AnimationController(
duration: const Duration(milliseconds: 1500), // 1.5
vsync: this,
);
//
_swipeController = AnimationController(
duration: const Duration(milliseconds: 550), // 500ms
vsync: this,
);
//
_swipeController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
widget.onAnimationCompleted();
}
});
//
_firstAnimation = Tween<Offset>(
begin: const Offset(1.5, 0.0), //
end: Offset.zero, //
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
//
_secondAnimation = Tween<Offset>(
begin: Offset.zero, //
end: const Offset(-1.5, 0.0), //
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
//
_swipeAnimation = Tween<Offset>(
begin: Offset.zero, //
end: const Offset(-1.5, 0.0), //
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_startAnimationSequence();
}
});
}
void _startAnimationSequence() async {
//
_controller.reset();
await _controller.forward();
// 3
await Future.delayed(const Duration(seconds: 3));
if (!mounted) return;
//
setState(() {
_firstPhaseCompleted = true;
});
_controller.reset();
await _controller.forward();
if (mounted && !_isSwipeAnimating) {
widget.onAnimationCompleted();
}
}
@override
void dispose() {
_controller.dispose();
_swipeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 使
if (_isSwipeAnimating) {
return SlideTransition(
position: _swipeAnimation,
child: _buildRocketAnimation(),
);
}
// 使
return SlideTransition(
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
child: _buildRocketAnimation(),
);
}
Widget _buildRocketAnimation() {
return GestureDetector(
onTap: () {
debouncer.debounce(
duration: const Duration(milliseconds: 350),
onDebounce: () {
final String targetRoomId = widget.message.roomId?.trim() ?? '';
final BuildContext? currentContext =
navigatorKey.currentState?.context;
if (targetRoomId.isEmpty || currentContext == null) {
return;
}
final String currentRoomId =
Provider.of<RealTimeCommunicationManager>(
currentContext,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id ??
'';
if (currentRoomId == targetRoomId) {
return;
}
widget.onAnimationCompleted();
ATRoomUtils.goRoom(targetRoomId, currentContext, fromFloting: true);
},
);
},
onHorizontalDragEnd: (details) {
//
final textDirection = Directionality.of(context);
//
double effectiveVelocity(double velocity) {
// RTL模式下
return textDirection == TextDirection.rtl ? -velocity : velocity;
}
double velocity = effectiveVelocity(details.primaryVelocity ?? 0);
if (velocity < 0) {
///
_handleSwipeLeft();
}
},
child: SizedBox(
width: ScreenUtil().screenWidth,
height: 148.w,
child: Align(
alignment: Alignment.topCenter,
child: Directionality(
textDirection: TextDirection.ltr,
child: Container(
width: 355.w,
height: 103.w,
margin: EdgeInsets.only(top: 45.w),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
"atu_images/room/at_icon_room_rocket_floating_bg.png",
),
fit: BoxFit.fill,
),
),
child: Stack(
children: [
Positioned(
left: 57.w,
top: 44.w,
width: 30.w,
height: 30.w,
child: _buildAvatar(),
),
Positioned(
left: 93.w,
top: 43.w,
width: 107.w,
child: Text(
_displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
height: 16 / 14,
decoration: TextDecoration.none,
),
),
),
Positioned(
left: 93.w,
top: 65.w,
width: 107.w,
child: Text(
_displaySubtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.61),
fontSize: 10.sp,
height: 1.2,
decoration: TextDecoration.none,
),
),
),
Positioned(
left: 205.w,
top: 40.w,
width: 36.w,
height: 36.w,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
"atu_images/room/at_icon_room_rocket_reward_item_bg.png",
fit: BoxFit.fill,
),
),
_buildRewardIcon(),
],
),
),
Positioned(
left: 250.w,
top: 47.w,
width: 51.w,
height: 23.w,
child: Image.asset(
"atu_images/room/at_icon_room_rocket_go.png",
fit: BoxFit.fill,
),
),
],
),
),
),
),
),
);
}
String get _displayName {
final value = widget.message.userName ?? "";
return value.isEmpty ? "Namenamna…" : value;
}
String get _displaySubtitle {
final value = widget.message.userId ?? "";
return value.isEmpty ? "ababababab" : value;
}
Widget _buildAvatar() {
final url = widget.message.userAvatarUrl ?? "";
return ClipOval(
child:
url.isEmpty
? Image.asset(
"atu_images/general/at_icon_avar_defalt.png",
fit: BoxFit.cover,
)
: netImage(
url: url,
width: 30.w,
height: 30.w,
shape: BoxShape.circle,
fit: BoxFit.cover,
),
);
}
Widget _buildRewardIcon() {
final url = widget.message.giftUrl ?? "";
if (url.isEmpty) {
return Image.asset(
"atu_images/room/at_icon_room_rocket_floating_rocket.png",
width: 20.w,
height: 29.w,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
);
}
return netImage(
url: url,
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
noDefaultImg: true,
);
}
//
void _handleSwipeLeft() {
if (_isSwipeAnimating) return;
setState(() {
_isSwipeAnimating = true;
});
//
_controller.stop();
//
_swipeController.reset();
_swipeController.forward();
}
}

View File

@ -0,0 +1,473 @@
import 'package:aslan/chatvibe_domain/models/res/at_room_rocket_reward_res.dart';
import 'package:aslan/chatvibe_ui/components/at_compontent.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class RoomRocketRewardPage extends StatelessWidget {
static const String dialogTag = 'showRoomRocketRewardDialog';
const RoomRocketRewardPage({
super.key,
required this.rewards,
required this.currentUserId,
this.igniterUserId = '',
});
final List<ATRoomRocketRewardRes> rewards;
final String currentUserId;
final String igniterUserId;
List<ATRoomRocketRewardRes> get _currentUserRewards => rewards
.where((reward) => reward.userId == currentUserId)
.toList(growable: false);
List<_RocketRewardWinner> get _winners {
final Map<String, List<ATRoomRocketRewardRes>> rewardsByUser =
<String, List<ATRoomRocketRewardRes>>{};
for (final reward in rewards) {
final String userId = reward.userId.trim();
if (userId.isEmpty || userId == '0') {
continue;
}
rewardsByUser
.putIfAbsent(userId, () => <ATRoomRocketRewardRes>[])
.add(reward);
}
final List<_RocketRewardWinner> winners = rewardsByUser.entries
.map((entry) {
final ATRoomRocketRewardRes firstReward = entry.value.first;
final bool isIgniter = entry.key == igniterUserId;
return _RocketRewardWinner(
userId: entry.key,
name:
firstReward.userNickname.isNotEmpty
? firstReward.userNickname
: 'User ${entry.key}',
avatarUrl: firstReward.userAvatar,
rewards: List<ATRoomRocketRewardRes>.unmodifiable(entry.value),
isIgniter: isIgniter,
);
})
.toList(growable: false);
winners.sort((left, right) {
if (left.isIgniter != right.isIgniter) {
return left.isIgniter ? -1 : 1;
}
return left.userId.compareTo(right.userId);
});
return winners;
}
@override
Widget build(BuildContext context) {
final List<ATRoomRocketRewardRes> currentRewards = _currentUserRewards;
final List<_RocketRewardWinner> winners = _winners;
return Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 360.w,
height: 576.w,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'atu_images/room/at_icon_room_rocket_record_bg.png',
fit: BoxFit.fill,
),
),
const Positioned(
top: 3,
left: 0,
right: 0,
child: _RocketRewardText(
'Reward',
fontSize: 16,
fontWeight: FontWeight.w500,
textAlign: TextAlign.center,
),
),
Positioned(
top: 30.w,
right: 24.w,
width: 24.w,
height: 23.w,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => SmartDialog.dismiss(tag: dialogTag),
child: Image.asset(
'atu_images/room/at_icon_room_rocket_record_close.png',
fit: BoxFit.contain,
),
),
),
const Positioned(
top: 39,
left: 0,
right: 0,
child: _RocketRewardSectionTitle(
label: 'Congratulations on winning',
),
),
if (currentRewards.isNotEmpty)
Positioned(
top: 65.w,
left: 20.w,
right: 19.w,
height: 206.w,
child: _buildPrizeGrid(currentRewards),
)
else ...[
Positioned(
top: 88.w,
left: 117.w,
width: 125.w,
height: 126.w,
child: Image.asset(
'atu_images/room/at_icon_room_rocket_reward_empty.png',
fit: BoxFit.fill,
),
),
const Positioned(
top: 240,
left: 0,
right: 0,
child: _RocketRewardText(
"You didn't win a prize. Better luck next time!",
fontSize: 14,
textAlign: TextAlign.center,
),
),
],
Positioned(
top: 291.w,
left: 0,
right: 0,
child: _RocketRewardSectionTitle(
label: '${winners.length} users received rewards',
),
),
Positioned(
top: 309.w,
left: 16.w,
right: 11.w,
height: 204.w,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: winners.length,
itemExtent: 51.w,
itemBuilder:
(_, index) => _RocketRewardWinnerRow(
winner: winners[index],
showLargeFrame: index < 3,
),
),
),
],
),
),
);
}
Widget _buildPrizeGrid(List<ATRoomRocketRewardRes> currentRewards) {
return GridView.builder(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12.w,
crossAxisSpacing: 15.w,
childAspectRatio: 1,
),
itemCount: currentRewards.length,
itemBuilder:
(_, index) => _RocketRewardPrize(reward: currentRewards[index]),
);
}
}
class _RocketRewardPrize extends StatelessWidget {
const _RocketRewardPrize({required this.reward});
final ATRoomRocketRewardRes reward;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
'atu_images/room/at_icon_room_rocket_reward_item_bg.png',
fit: BoxFit.fill,
),
),
Positioned(
top: 11.w,
width: 54.w,
height: 54.w,
child: _RocketRewardIcon(reward: reward, size: 54.w),
),
Positioned(
bottom: 10.w,
left: 2.w,
right: 2.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (reward.isCoin) ...[
Image.asset(
'atu_images/room/at_icon_room_rocket_reward_coin.png',
width: 12.w,
height: 12.w,
),
SizedBox(width: 1.w),
],
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: _RocketRewardText(
reward.displayLabel,
fontSize: 12,
maxLines: 1,
),
),
),
],
),
),
],
);
}
}
class _RocketRewardWinnerRow extends StatelessWidget {
const _RocketRewardWinnerRow({
required this.winner,
required this.showLargeFrame,
});
final _RocketRewardWinner winner;
final bool showLargeFrame;
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 50.w,
height: 50.w,
child: Stack(
alignment: Alignment.center,
children: [
_RocketRewardAvatar(url: winner.avatarUrl, size: 30.w),
if (showLargeFrame)
Image.asset(
'atu_images/room/at_icon_room_rocket_reward_avatar_frame.png',
width: 50.w,
height: 50.w,
fit: BoxFit.contain,
),
],
),
),
Expanded(
child: _RocketRewardText(
winner.name,
fontSize: 14,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 8.w),
...winner.rewards
.take(4)
.map(
(reward) => Padding(
padding: EdgeInsets.only(left: 5.w),
child: _RocketRewardMiniPrize(reward: reward),
),
),
],
);
}
}
class _RocketRewardMiniPrize extends StatelessWidget {
const _RocketRewardMiniPrize({required this.reward});
final ATRoomRocketRewardRes reward;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 31.w,
height: 32.w,
child: Column(
children: [
_RocketRewardIcon(reward: reward, size: 22.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (reward.isCoin)
Image.asset(
'atu_images/room/at_icon_room_rocket_reward_coin.png',
width: 6.w,
height: 6.w,
),
SizedBox(
width: reward.isCoin ? 20.w : 27.w,
child: FittedBox(
fit: BoxFit.scaleDown,
child: _RocketRewardText(
reward.displayLabel,
fontSize: 8,
maxLines: 1,
),
),
),
],
),
],
),
);
}
}
class _RocketRewardIcon extends StatelessWidget {
const _RocketRewardIcon({required this.reward, required this.size});
final ATRoomRocketRewardRes reward;
final double size;
@override
Widget build(BuildContext context) {
if (reward.cover.isNotEmpty) {
return netImage(
url: reward.cover,
width: size,
height: size,
fit: BoxFit.contain,
noDefaultImg: true,
);
}
return Image.asset(
reward.isCoin
? 'atu_images/room/at_icon_room_rocket_reward_coin.png'
: 'atu_images/room/at_icon_room_rocket_reward_gift.png',
width: size,
height: size,
fit: BoxFit.contain,
);
}
}
class _RocketRewardAvatar extends StatelessWidget {
const _RocketRewardAvatar({required this.url, required this.size});
final String url;
final double size;
@override
Widget build(BuildContext context) {
return ClipOval(
child:
url.isEmpty
? Image.asset(
'atu_images/general/at_icon_avar_defalt.png',
width: size,
height: size,
fit: BoxFit.cover,
)
: netImage(
url: url,
width: size,
height: size,
shape: BoxShape.circle,
fit: BoxFit.cover,
),
);
}
}
class _RocketRewardSectionTitle extends StatelessWidget {
const _RocketRewardSectionTitle({required this.label});
final String label;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Transform(
alignment: Alignment.center,
transform: Matrix4.diagonal3Values(-1, 1, 1),
child: const _RocketRewardText('///', fontSize: 10, letterSpacing: 1),
),
SizedBox(width: 4.w),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: _RocketRewardText(label, fontSize: 14),
),
),
SizedBox(width: 4.w),
const _RocketRewardText('///', fontSize: 10, letterSpacing: 1),
],
);
}
}
class _RocketRewardText extends StatelessWidget {
const _RocketRewardText(
this.value, {
required this.fontSize,
this.fontWeight = FontWeight.w400,
this.textAlign,
this.maxLines,
this.overflow,
this.letterSpacing,
});
final String value;
final double fontSize;
final FontWeight fontWeight;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final double? letterSpacing;
@override
Widget build(BuildContext context) {
return Text(
value,
maxLines: maxLines,
overflow: overflow,
textAlign: textAlign,
style: TextStyle(
color: Colors.white,
fontSize: fontSize.sp,
fontWeight: fontWeight,
height: 1,
letterSpacing: letterSpacing,
decoration: TextDecoration.none,
),
);
}
}
class _RocketRewardWinner {
const _RocketRewardWinner({
required this.userId,
required this.name,
required this.avatarUrl,
required this.rewards,
required this.isIgniter,
});
final String userId;
final String name;
final String avatarUrl;
final List<ATRoomRocketRewardRes> rewards;
final bool isIgniter;
}

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:aslan/app_localizations.dart';
import 'package:aslan/chatvibe_core/constants/at_global_config.dart';
import 'package:aslan/chatvibe_data/models/enum/at_activity_reward_props_type.dart';
import 'package:aslan/chatvibe_data/sources/repositories/room_repository_imp.dart';
import 'package:aslan/chatvibe_domain/models/res/at_room_rocket_config_res.dart';
@ -166,19 +165,22 @@ class _RoomRocketYellowPageState extends State<RoomRocketYellowPage> {
height: 14.w,
child: Consumer<RtcProvider>(
builder: (context, ref, child) {
final progress = _progressValue(ref.roomRocketStatus);
final status = ref.roomRocketStatus;
final currentEnergy = _currentEnergyValue(status);
final maxEnergy = _selectedLevelMaxEnergy;
final progress = _progressValue(status);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
text(
"${ref.roomRocketStatus?.currentEnergy ?? 0}/${currernRocketLevel?.maxEnergy ?? 0}",
"$currentEnergy/$maxEnergy",
textColor: Color(0xFFFFDC37),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
SizedBox(width: 10.w),
text(
"${(progress * 100).clamp(0, 100).toInt()}%",
"${_progressPercentText(progress)}%",
textColor: Color(0xFFFFDC37),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
@ -355,7 +357,9 @@ class _RoomRocketYellowPageState extends State<RoomRocketYellowPage> {
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFFFF1A3), Color(0xFFFFA000)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF59B1D), Color(0xFFFFEF9E)],
),
),
),
@ -384,13 +388,25 @@ class _RoomRocketYellowPageState extends State<RoomRocketYellowPage> {
}
double _progressValue(ATRoomRocketStatusRes? status) {
final num current =
status?.currentEnergy ?? roomRocketStatus?.currentEnergy ?? 0;
final num max = currernRocketLevel?.maxEnergy ?? status?.maxEnergy ?? 0;
final num current = _currentEnergyValue(status);
final num max = _selectedLevelMaxEnergy;
if (max <= 0) return 0;
return (current / max).clamp(0.0, 1.0).toDouble();
}
num _currentEnergyValue(ATRoomRocketStatusRes? status) {
return status?.currentEnergy ?? roomRocketStatus?.currentEnergy ?? 0;
}
num get _selectedLevelMaxEnergy => currernRocketLevel?.maxEnergy ?? 0;
String _progressPercentText(double progress) {
return (progress * 100)
.clamp(0, 100)
.toStringAsFixed(2)
.replaceFirst(RegExp(r'\.?0+$'), '');
}
Widget _buildRewardTabs() {
final l10n = ATAppLocalizations.of(context)!;
final labels = [
@ -523,45 +539,11 @@ class _RoomRocketYellowPageState extends State<RoomRocketYellowPage> {
void _showHelpDialog() {
SmartDialog.show(
tag: "showRoomRocketHelpDialog",
tag: "showRoomRocketNoteDialog",
alignment: Alignment.center,
debounce: true,
animationType: SmartAnimationType.fade,
builder: (_) {
return Container(
width: ScreenUtil().screenWidth,
margin: EdgeInsets.symmetric(horizontal: 15.w),
padding: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
"atu_images/room/at_icon_room_rocket_hepl_bg.png",
),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 13.w),
Image.asset(
ATGlobalConfig.lang == "ar"
? "atu_images/room/at_icon_room_rocket_rule_text_ar.png"
: "atu_images/room/at_icon_room_rocket_rule_text_en.png",
width: 58.w,
),
SizedBox(height: 8.w),
text(
ATAppLocalizations.of(context)!.roomRocketHelpTips,
maxLines: 25,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
SizedBox(height: 20.w),
],
),
);
},
builder: (_) => const _RoomRocketNoteDialog(),
);
}
@ -576,6 +558,80 @@ class _RoomRocketYellowPageState extends State<RoomRocketYellowPage> {
}
}
class _RoomRocketNoteDialog extends StatelessWidget {
const _RoomRocketNoteDialog();
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: SizedBox(
width: 360.w,
height: 576.w,
child: Stack(
children: [
Positioned.fill(
child: IgnorePointer(
child: Image.asset(
"atu_images/room/at_icon_room_rocket_record_bg.png",
fit: BoxFit.fill,
),
),
),
PositionedDirectional(
top: 3.w,
start: 0,
end: 0,
child: text(
ATAppLocalizations.of(context)!.roomRocketNoteTitle,
textColor: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
),
PositionedDirectional(
top: 27.w,
end: 24.w,
width: 24.w,
height: 23.w,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
SmartDialog.dismiss(tag: "showRoomRocketNoteDialog");
},
child: Image.asset(
"atu_images/room/at_icon_room_rocket_record_close.png",
fit: BoxFit.contain,
),
),
),
PositionedDirectional(
top: 49.w,
start: 27.w,
end: 27.w,
bottom: 27.w,
child: SingleChildScrollView(
child: Text(
ATAppLocalizations.of(context)!.roomRocketHelpTips,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w400,
height: 1.4,
decoration: TextDecoration.none,
),
textAlign: TextAlign.start,
),
),
),
],
),
),
);
}
}
class _RoomRocketRecordDialog extends StatelessWidget {
const _RoomRocketRecordDialog();
@ -597,7 +653,7 @@ class _RoomRocketRecordDialog extends StatelessWidget {
),
),
PositionedDirectional(
top: 12.w,
top: 3.w,
start: 0,
end: 0,
child: text(
@ -609,19 +665,18 @@ class _RoomRocketRecordDialog extends StatelessWidget {
),
),
PositionedDirectional(
top: 27.w,
top: 30.w,
end: 24.w,
width: 24.w,
height: 24.w,
height: 23.w,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
SmartDialog.dismiss(tag: "showRoomRocketRecordDialog");
},
child: Image.asset(
"atu_images/room/at_icon_room_game_close.png",
"atu_images/room/at_icon_room_rocket_record_close.png",
fit: BoxFit.contain,
color: Color(0xFFFFF3A8),
),
),
),
@ -647,11 +702,22 @@ class _RoomRocketRecordDialog extends StatelessWidget {
end: 28.w,
bottom: 44.w,
child: Center(
child: text(
ATAppLocalizations.of(context)!.noData,
textColor: Color(0xFFFFFFFF),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"atu_images/general/at_icon_no_data_icon.png",
width: 121.w,
fit: BoxFit.fitWidth,
),
SizedBox(height: 10.w),
text(
ATAppLocalizations.of(context)!.roomRocketRecordEmpty,
textColor: Color(0xFFFFFFFF),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
],
),
),
),

View File

@ -0,0 +1,167 @@
import 'package:aslan/chatvibe_data/models/message/at_floating_message.dart';
import 'package:aslan/chatvibe_domain/models/res/at_room_rocket_reward_res.dart';
import 'package:aslan/chatvibe_ui/widgets/room/rocket/room_rocket_floating_popup.dart';
import 'package:aslan/chatvibe_ui/widgets/room/rocket/room_rocket_reward_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestWidgetsFlutterBinding
.instance
.platformDispatcher
.views
.first
.physicalSize = const Size(375, 812);
TestWidgetsFlutterBinding
.instance
.platformDispatcher
.views
.first
.devicePixelRatio = 1;
});
tearDown(() {
TestWidgetsFlutterBinding.instance.platformDispatcher.views.first
.resetPhysicalSize();
TestWidgetsFlutterBinding.instance.platformDispatcher.views.first
.resetDevicePixelRatio();
});
test('rocket reward parses the current backend API and IM fields', () {
final ATRoomRocketRewardRes apiReward =
ATRoomRocketRewardRes.fromJson(<String, dynamic>{
'rewardType': 'GOLD',
'rewardItemId': 12,
'rewardItemName': 'Gold',
'rewardAmount': 18800,
'cover': 'https://example.test/gold.png',
'rocketLevel': 2,
'message': 'Congratulations',
});
final ATRoomRocketRewardRes messageReward =
ATRoomRocketRewardRes.fromJson(<String, dynamic>{
'rewardType': 'PROP',
'cover': 'https://example.test/frame.png',
'rocketLevel': 3,
'userId': 10002,
'userNickname': 'Rocket Cat',
});
expect(apiReward.rewardItemId, '12');
expect(apiReward.displayLabel, '18800');
expect(messageReward.userId, '10002');
expect(messageReward.userNickname, 'Rocket Cat');
});
testWidgets(
'rocket floating window no longer opens the reward dialog on tap',
(tester) async {
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder:
(_, __) => MaterialApp(
home: Scaffold(
body: RoomRocketFloatingPopup(
message: ATFloatingMessage(
type: 3,
roomId: '',
userName: 'Igniter',
userId: '163526',
rocketLevel: 2,
),
onAnimationCompleted: () {},
),
),
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 1500));
expect(find.text('Igniter'), findsOneWidget);
await tester.tap(find.byType(RoomRocketFloatingPopup));
await tester.pump(const Duration(milliseconds: 400));
expect(find.byType(RoomRocketRewardPage), findsNothing);
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(seconds: 4));
},
);
testWidgets(
'rocket reward panel binds winner count and current user rewards',
(tester) async {
await tester.pumpWidget(
_testApp(
RoomRocketRewardPage(
rewards: _sampleRewards(),
currentUserId: '10002',
igniterUserId: '10003',
),
),
);
expect(find.text('10000'), findsNWidgets(2));
expect(find.text('2 users received rewards'), findsOneWidget);
expect(find.text('Rocket Cat'), findsOneWidget);
expect(find.text('Igniter'), findsOneWidget);
},
);
testWidgets('same reward panel switches to not-won state for another user', (
tester,
) async {
await tester.pumpWidget(
_testApp(
RoomRocketRewardPage(
rewards: _sampleRewards(),
currentUserId: '99999',
igniterUserId: '10003',
),
),
);
expect(
find.text("You didn't win a prize. Better luck next time!"),
findsOneWidget,
);
expect(find.text('2 users received rewards'), findsOneWidget);
});
}
Widget _testApp(Widget child) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder:
(_, __) => MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: Center(child: child),
),
),
);
}
List<ATRoomRocketRewardRes> _sampleRewards() {
return <ATRoomRocketRewardRes>[
ATRoomRocketRewardRes.fromJson(<String, dynamic>{
'userId': 10002,
'userNickname': 'Rocket Cat',
'rewardType': 'GOLD',
'rewardAmount': 10000,
}),
ATRoomRocketRewardRes.fromJson(<String, dynamic>{
'userId': 10003,
'userNickname': 'Igniter',
'rewardType': 'PROP',
'rewardItemName': '7 days',
}),
];
}