1.3.0 初版
@ -327,9 +327,42 @@
|
||||
"redEnvelopeRecTips2": "تم المطالبة بجميع المظاريف الحمراء.",
|
||||
"redEnvelopeRecTips3": "انتهى وقت جمع الظرف الأحمر!",
|
||||
"openTheTreasureChest": "افتح صندوق الكنز",
|
||||
"redEnvelopeRecTips1": "تم إيداع العملات المكتسبة في محفظتك.",
|
||||
"redEnvelope": "ظرف أحمر",
|
||||
"redEnvelopeTips1": "عملات:",
|
||||
"redEnvelopeRecTips1": "تم إيداع العملات المكتسبة في محفظتك.",
|
||||
"redEnvelope": "ظرف أحمر",
|
||||
"roomRedPacketGoldCoins": "عملات ذهبية",
|
||||
"roomRedPacketQuantity": "العدد",
|
||||
"roomRedPacketPickupTime": "وقت الاستلام",
|
||||
"roomRedPacketInMinutes": "بعد {1} دقائق",
|
||||
"roomRedPacketClaimNow": "استلم الآن",
|
||||
"roomRedPacketSendLuckyPack": "أرسل حزمة الحظ",
|
||||
"roomRedPacketSending": "جارٍ الإرسال...",
|
||||
"roomRedPacketSendSuccess": "تم إرسال حزمة الحظ",
|
||||
"roomRedPacketComingSoon": "قريباً",
|
||||
"roomRedPacketRulesTitle": "القواعد",
|
||||
"roomRedPacketRulesContent": "1. يمكن لأعضاء الغرفة استلام حزمة الحظ بعد وقت الاستلام المحدد.\n2. سيتم خصم العملات بعد الإرسال، ويمكن إرجاع العملات غير المستلمة بعد انتهاء النشاط.\n3. يرجى الحفاظ على تفاعل ودي داخل الغرفة وعدم استخدام حزم الحظ للرسائل المزعجة أو الاحتيال.\n4. سيتم تحديث النص النهائي للقواعد بعد تأكيد واجهة الخادم.",
|
||||
"roomRedPacketHistoryTitle": "سجل إرسال حزم الحظ",
|
||||
"roomRedPacketHistoryItemTitle": "حزمة حظ ({1} مظروفاً)",
|
||||
"roomRedPacketHistoryStatusSent": "تم الإرسال",
|
||||
"roomRedPacketHistoryStatusPending": "قيد الانتظار",
|
||||
"roomRedPacketHistoryStatusReturned": "تم الإرجاع",
|
||||
"roomRedPacketChatMessage": "أرسلت لك مظروفاً أحمر، افتحه الآن!",
|
||||
"roomRedPacketReceivedMessage": "استلم {1} عملة من حزمة الحظ {2}",
|
||||
"roomRedPacketOpenSubtitle": "وصلت حزمة الحظ",
|
||||
"roomRedPacketOpenYouGot": "حصلت على",
|
||||
"roomRedPacketClaimSuccessTips": "تم إرسال العملات إلى محفظتك.",
|
||||
"roomRedPacketOpenSoldOut": "تم استلام حزمة الحظ بالكامل.",
|
||||
"roomRedPacketOpenExpired": "انتهت صلاحية حزمة الحظ.",
|
||||
"roomRedPacketRegionNotMatch": "يمكن فقط للمستخدمين في نفس منطقة اللغة استلام حزمة الحظ.",
|
||||
"roomRedPacketNotStarted": "حزمة الحظ غير جاهزة بعد.",
|
||||
"roomRedPacketDailyLimitExceeded": "تم الوصول إلى حد الإرسال اليومي.",
|
||||
"roomRedPacketInsufficientBalance": "العملات الذهبية غير كافية.",
|
||||
"roomRedPacketPresenceRequired": "يرجى البقاء داخل الغرفة لاستخدام حزم الحظ.",
|
||||
"roomRedPacketNetworkError": "تعذر معالجة حزمة الحظ الآن.",
|
||||
"roomRedPacketOpenWaiting": "يمكن فتحها بعد {1} دقائق.",
|
||||
"roomRedPacketOkay": "حسناً",
|
||||
"roomRedPacketFloatingTitle": "{1} أرسل مظروفاً أحمر",
|
||||
"roomRedPacketFloatingSubtitle": "أرسل مظروفاً أحمر",
|
||||
"redEnvelopeTips1": "عملات:",
|
||||
"roomTools": "أدوات الغرفة:",
|
||||
"entertainment": "الترفيه:",
|
||||
"reportSucc": "تم الإبلاغ بنجاح",
|
||||
|
||||
@ -177,6 +177,39 @@
|
||||
"sendRedPackConfirmTips": "আপনি কি লাল প্যাক পাঠাতে চান?",
|
||||
"redEnvelopeSendingRecords": "লাল খাম প্রেরণ রেকর্ড:",
|
||||
"redEnvelope": "লাল খাম",
|
||||
"roomRedPacketGoldCoins": "স্বর্ণ কয়েন",
|
||||
"roomRedPacketQuantity": "সংখ্যা",
|
||||
"roomRedPacketPickupTime": "গ্রহণের সময়",
|
||||
"roomRedPacketInMinutes": "{1} মিনিট পরে",
|
||||
"roomRedPacketClaimNow": "এখন গ্রহণ",
|
||||
"roomRedPacketSendLuckyPack": "লাকি প্যাক পাঠান",
|
||||
"roomRedPacketSending": "পাঠানো হচ্ছে...",
|
||||
"roomRedPacketSendSuccess": "লাকি প্যাক পাঠানো হয়েছে",
|
||||
"roomRedPacketComingSoon": "শীঘ্রই আসছে",
|
||||
"roomRedPacketRulesTitle": "নিয়ম",
|
||||
"roomRedPacketRulesContent": "1. নির্বাচিত গ্রহণের সময় শেষ হলে রুমের সদস্যরা প্রতিটি লাকি প্যাক গ্রহণ করতে পারবেন।\n2. পাঠানোর পরে কয়েন কেটে নেওয়া হবে। কার্যক্রম শেষ হলে অব্যবহৃত কয়েন ফেরত দেওয়া যেতে পারে।\n3. রুমের যোগাযোগ বন্ধুত্বপূর্ণ রাখুন এবং স্প্যাম বা প্রতারণার জন্য লাকি প্যাক ব্যবহার করবেন না।\n4. ব্যাকএন্ড API নিশ্চিত হওয়ার পরে চূড়ান্ত নিয়মের লেখা আপডেট করা হবে।",
|
||||
"roomRedPacketHistoryTitle": "লাকি প্যাক পাঠানোর ইতিহাস",
|
||||
"roomRedPacketHistoryItemTitle": "লাকি প্যাক ({1} খাম)",
|
||||
"roomRedPacketHistoryStatusSent": "পাঠানো হয়েছে",
|
||||
"roomRedPacketHistoryStatusPending": "অপেক্ষায়",
|
||||
"roomRedPacketHistoryStatusReturned": "ফেরত দেওয়া হয়েছে",
|
||||
"roomRedPacketChatMessage": "আমি তোমাকে একটি লাল খাম পাঠিয়েছি, এখনই খোলো!",
|
||||
"roomRedPacketReceivedMessage": "{2} লাকি প্যাক থেকে {1} কয়েন গ্রহণ করুন",
|
||||
"roomRedPacketOpenSubtitle": "লাকি প্যাক এসেছে",
|
||||
"roomRedPacketOpenYouGot": "তুমি পেয়েছ",
|
||||
"roomRedPacketClaimSuccessTips": "কয়েন তোমার ওয়ালেটে পাঠানো হয়েছে।",
|
||||
"roomRedPacketOpenSoldOut": "লাকি প্যাক ইতিমধ্যে শেষ হয়েছে।",
|
||||
"roomRedPacketOpenExpired": "লাকি প্যাকের সময় শেষ হয়েছে।",
|
||||
"roomRedPacketRegionNotMatch": "শুধু একই ভাষা অঞ্চলের ব্যবহারকারীরা এই লাকি প্যাক গ্রহণ করতে পারবেন।",
|
||||
"roomRedPacketNotStarted": "লাকি প্যাক এখনো প্রস্তুত নয়।",
|
||||
"roomRedPacketDailyLimitExceeded": "আজকের পাঠানোর সীমা শেষ হয়েছে।",
|
||||
"roomRedPacketInsufficientBalance": "স্বর্ণ কয়েন যথেষ্ট নয়।",
|
||||
"roomRedPacketPresenceRequired": "লাকি প্যাক ব্যবহার করতে রুমে থাকুন।",
|
||||
"roomRedPacketNetworkError": "এখন লাকি প্যাক প্রক্রিয়া করা যাচ্ছে না।",
|
||||
"roomRedPacketOpenWaiting": "{1} মিনিট পরে খোলা যাবে।",
|
||||
"roomRedPacketOkay": "ঠিক আছে",
|
||||
"roomRedPacketFloatingTitle": "{1} একটি লাল খাম পাঠিয়েছে",
|
||||
"roomRedPacketFloatingSubtitle": "একটি লাল খাম পাঠিয়েছে",
|
||||
"redEnvelopeRecTips2": "সব লাল খাম দাবি করা হয়েছে।",
|
||||
"redEnvelopeRecTips3": "লাল খাম সংগ্রহের সময় শেষ হয়ে গেছে!",
|
||||
"openTheTreasureChest": "সম্পদ বাক্স খুলুন",
|
||||
|
||||
@ -148,6 +148,39 @@
|
||||
"sendRedPackConfirmTips": "Are you sure you want to send the red packet?",
|
||||
"redEnvelopeSendingRecords": "Red envelope sending records:",
|
||||
"redEnvelope": "Red Envelope",
|
||||
"roomRedPacketGoldCoins": "Gold Coins",
|
||||
"roomRedPacketQuantity": "Quantity",
|
||||
"roomRedPacketPickupTime": "Pickup Time",
|
||||
"roomRedPacketInMinutes": "In {1} Minutes",
|
||||
"roomRedPacketClaimNow": "Claim Now",
|
||||
"roomRedPacketSendLuckyPack": "Send A Lucky Pack",
|
||||
"roomRedPacketSending": "Sending...",
|
||||
"roomRedPacketSendSuccess": "Lucky pack sent",
|
||||
"roomRedPacketComingSoon": "Coming soon",
|
||||
"roomRedPacketRulesTitle": "Rules",
|
||||
"roomRedPacketRulesContent": "1. Each lucky pack can be claimed by room members after the selected pickup time.\n2. Coins are deducted after sending. Unclaimed coins can be returned after the activity ends.\n3. Please keep room interaction friendly and do not use lucky packs for spam or fraud.\n4. Final rule text will be updated after the backend API is confirmed.",
|
||||
"roomRedPacketHistoryTitle": "Lucky Pack Delivery History",
|
||||
"roomRedPacketHistoryItemTitle": "Lucky Bag ({1} red envelopes)",
|
||||
"roomRedPacketHistoryStatusSent": "Sent",
|
||||
"roomRedPacketHistoryStatusPending": "Pending",
|
||||
"roomRedPacketHistoryStatusReturned": "Returned",
|
||||
"roomRedPacketChatMessage": "I sent you a red envelope, open it right away!",
|
||||
"roomRedPacketReceivedMessage": "Claim {1} coins from the {2} Lucky Pack",
|
||||
"roomRedPacketOpenSubtitle": "The Lucky Pack Has Arrived",
|
||||
"roomRedPacketOpenYouGot": "You Got",
|
||||
"roomRedPacketClaimSuccessTips": "Coins have been sent to your wallet.",
|
||||
"roomRedPacketOpenSoldOut": "The Lucky Pack Has Already Sold Out.",
|
||||
"roomRedPacketOpenExpired": "The Lucky Pack Has Expired.",
|
||||
"roomRedPacketRegionNotMatch": "Only users in the same language region can claim this lucky pack.",
|
||||
"roomRedPacketNotStarted": "The lucky pack is not ready yet.",
|
||||
"roomRedPacketDailyLimitExceeded": "Today's sending limit has been reached.",
|
||||
"roomRedPacketInsufficientBalance": "Insufficient gold coins.",
|
||||
"roomRedPacketPresenceRequired": "Please stay in the room to use lucky packs.",
|
||||
"roomRedPacketNetworkError": "Unable to process the lucky pack right now.",
|
||||
"roomRedPacketOpenWaiting": "Can be opened in {1} minutes.",
|
||||
"roomRedPacketOkay": "Okay",
|
||||
"roomRedPacketFloatingTitle": "{1} sent a red envelope",
|
||||
"roomRedPacketFloatingSubtitle": "Sent a red envelope",
|
||||
"redEnvelopeRecTips2": "The red envelopes have all been claimed.",
|
||||
"redEnvelopeRecTips3": "The red envelope collection time has expired!",
|
||||
"openTheTreasureChest": "Open the treasure chest",
|
||||
|
||||
@ -135,9 +135,42 @@
|
||||
"collectionTimeTips": "Toplama Zamanı:{1}({2}/{3})",
|
||||
"sendARedEnvelope": "Kırmızı Zarf Gönder",
|
||||
"sendRedPackConfirmTips": "Kırmızı zarfı göndermek istediğinizden emin misiniz?",
|
||||
"redEnvelopeSendingRecords": "Kırmızı zarf gönderim kayıtları:",
|
||||
"redEnvelope": "Kırmızı Zarf",
|
||||
"redEnvelopeRecTips2": "Kırmızı zarfların hepsi talep edildi.",
|
||||
"redEnvelopeSendingRecords": "Kırmızı zarf gönderim kayıtları:",
|
||||
"redEnvelope": "Kırmızı Zarf",
|
||||
"roomRedPacketGoldCoins": "Altın Jeton",
|
||||
"roomRedPacketQuantity": "Adet",
|
||||
"roomRedPacketPickupTime": "Alma Zamanı",
|
||||
"roomRedPacketInMinutes": "{1} dk sonra",
|
||||
"roomRedPacketClaimNow": "Şimdi Al",
|
||||
"roomRedPacketSendLuckyPack": "Şans Paketi Gönder",
|
||||
"roomRedPacketSending": "Gönderiliyor...",
|
||||
"roomRedPacketSendSuccess": "Şans paketi gönderildi",
|
||||
"roomRedPacketComingSoon": "Yakında",
|
||||
"roomRedPacketRulesTitle": "Kurallar",
|
||||
"roomRedPacketRulesContent": "1. Her şans paketi, seçilen alma süresinden sonra oda üyeleri tarafından alınabilir.\n2. Gönderimden sonra jetonlar düşülür. Alınmayan jetonlar etkinlik bittikten sonra iade edilebilir.\n3. Lütfen oda etkileşimini dostça tutun ve şans paketlerini spam veya dolandırıcılık için kullanmayın.\n4. Nihai kural metni backend API onaylandıktan sonra güncellenecek.",
|
||||
"roomRedPacketHistoryTitle": "Şans Paketi Gönderim Geçmişi",
|
||||
"roomRedPacketHistoryItemTitle": "Şans Paketi ({1} zarf)",
|
||||
"roomRedPacketHistoryStatusSent": "Gönderildi",
|
||||
"roomRedPacketHistoryStatusPending": "Beklemede",
|
||||
"roomRedPacketHistoryStatusReturned": "İade edildi",
|
||||
"roomRedPacketChatMessage": "Sana bir kırmızı zarf gönderdim, hemen aç!",
|
||||
"roomRedPacketReceivedMessage": "{2} Şans Paketinden {1} jeton al",
|
||||
"roomRedPacketOpenSubtitle": "Şans Paketi Geldi",
|
||||
"roomRedPacketOpenYouGot": "Kazandın",
|
||||
"roomRedPacketClaimSuccessTips": "Jetonlar cüzdanına gönderildi.",
|
||||
"roomRedPacketOpenSoldOut": "Şans Paketi tamamen alındı.",
|
||||
"roomRedPacketOpenExpired": "Şans Paketinin süresi doldu.",
|
||||
"roomRedPacketRegionNotMatch": "Bu şans paketini yalnızca aynı dil bölgesindeki kullanıcılar alabilir.",
|
||||
"roomRedPacketNotStarted": "Şans paketi henüz hazır değil.",
|
||||
"roomRedPacketDailyLimitExceeded": "Bugünkü gönderim sınırına ulaşıldı.",
|
||||
"roomRedPacketInsufficientBalance": "Altın jeton yetersiz.",
|
||||
"roomRedPacketPresenceRequired": "Şans paketlerini kullanmak için odada kalın.",
|
||||
"roomRedPacketNetworkError": "Şans paketi şu anda işlenemiyor.",
|
||||
"roomRedPacketOpenWaiting": "{1} dakika sonra açılabilir.",
|
||||
"roomRedPacketOkay": "Tamam",
|
||||
"roomRedPacketFloatingTitle": "{1} kırmızı zarf gönderdi",
|
||||
"roomRedPacketFloatingSubtitle": "Kırmızı zarf gönderdi",
|
||||
"redEnvelopeRecTips2": "Kırmızı zarfların hepsi talep edildi.",
|
||||
"redEnvelopeRecTips3": "Kırmızı zarf toplama zamanı doldu!",
|
||||
"openTheTreasureChest": "Hazine Sandığını Aç",
|
||||
"redEnvelopeRecTips1": "Kazanan jettonlar cüzdanınıza yatırıldı.",
|
||||
|
||||
@ -502,7 +502,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -511,7 +511,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -693,7 +693,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -702,7 +702,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -722,7 +722,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -731,7 +731,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -610,7 +610,94 @@ class SCAppLocalizations {
|
||||
|
||||
String get redEnvelope => translate('redEnvelope');
|
||||
|
||||
String get dice => translate('dice');
|
||||
String get roomRedPacketGoldCoins => translate('roomRedPacketGoldCoins');
|
||||
|
||||
String get roomRedPacketQuantity => translate('roomRedPacketQuantity');
|
||||
|
||||
String get roomRedPacketPickupTime => translate('roomRedPacketPickupTime');
|
||||
|
||||
String get roomRedPacketClaimNow => translate('roomRedPacketClaimNow');
|
||||
|
||||
String get roomRedPacketSendLuckyPack =>
|
||||
translate('roomRedPacketSendLuckyPack');
|
||||
|
||||
String get roomRedPacketSending => translate('roomRedPacketSending');
|
||||
|
||||
String get roomRedPacketSendSuccess => translate('roomRedPacketSendSuccess');
|
||||
|
||||
String get roomRedPacketComingSoon => translate('roomRedPacketComingSoon');
|
||||
|
||||
String roomRedPacketInMinutes(String minutes) =>
|
||||
translate('roomRedPacketInMinutes').replaceAll('{1}', minutes);
|
||||
|
||||
String get roomRedPacketRulesTitle => translate('roomRedPacketRulesTitle');
|
||||
|
||||
String get roomRedPacketRulesContent =>
|
||||
translate('roomRedPacketRulesContent');
|
||||
|
||||
String get roomRedPacketHistoryTitle =>
|
||||
translate('roomRedPacketHistoryTitle');
|
||||
|
||||
String roomRedPacketHistoryItemTitle(String count) =>
|
||||
translate('roomRedPacketHistoryItemTitle').replaceAll('{1}', count);
|
||||
|
||||
String get roomRedPacketHistoryStatusSent =>
|
||||
translate('roomRedPacketHistoryStatusSent');
|
||||
|
||||
String get roomRedPacketHistoryStatusPending =>
|
||||
translate('roomRedPacketHistoryStatusPending');
|
||||
|
||||
String get roomRedPacketHistoryStatusReturned =>
|
||||
translate('roomRedPacketHistoryStatusReturned');
|
||||
|
||||
String get roomRedPacketChatMessage => translate('roomRedPacketChatMessage');
|
||||
|
||||
String roomRedPacketReceivedMessage(String amount, String name) => translate(
|
||||
'roomRedPacketReceivedMessage',
|
||||
).replaceAll('{1}', amount).replaceAll('{2}', name);
|
||||
|
||||
String get roomRedPacketOpenSubtitle =>
|
||||
translate('roomRedPacketOpenSubtitle');
|
||||
|
||||
String get roomRedPacketOpenYouGot => translate('roomRedPacketOpenYouGot');
|
||||
|
||||
String get roomRedPacketClaimSuccessTips =>
|
||||
translate('roomRedPacketClaimSuccessTips');
|
||||
|
||||
String get roomRedPacketOpenSoldOut => translate('roomRedPacketOpenSoldOut');
|
||||
|
||||
String get roomRedPacketOpenExpired => translate('roomRedPacketOpenExpired');
|
||||
|
||||
String get roomRedPacketRegionNotMatch =>
|
||||
translate('roomRedPacketRegionNotMatch');
|
||||
|
||||
String get roomRedPacketNotStarted =>
|
||||
translate('roomRedPacketNotStarted');
|
||||
|
||||
String get roomRedPacketDailyLimitExceeded =>
|
||||
translate('roomRedPacketDailyLimitExceeded');
|
||||
|
||||
String get roomRedPacketInsufficientBalance =>
|
||||
translate('roomRedPacketInsufficientBalance');
|
||||
|
||||
String get roomRedPacketPresenceRequired =>
|
||||
translate('roomRedPacketPresenceRequired');
|
||||
|
||||
String get roomRedPacketNetworkError =>
|
||||
translate('roomRedPacketNetworkError');
|
||||
|
||||
String roomRedPacketOpenWaiting(String minutes) =>
|
||||
translate('roomRedPacketOpenWaiting').replaceAll('{1}', minutes);
|
||||
|
||||
String get roomRedPacketOkay => translate('roomRedPacketOkay');
|
||||
|
||||
String roomRedPacketFloatingTitle(String name) =>
|
||||
translate('roomRedPacketFloatingTitle').replaceAll('{1}', name);
|
||||
|
||||
String get roomRedPacketFloatingSubtitle =>
|
||||
translate('roomRedPacketFloatingSubtitle');
|
||||
|
||||
String get dice => translate('dice');
|
||||
|
||||
String get rps => translate('rps');
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -53,14 +54,43 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
|
||||
|
||||
String _resolveVipChatBubbleImageUrl(SCVipResourceRes? resource) {
|
||||
return _firstNonBlankChatBubbleUrl([
|
||||
resource?.previewUrl,
|
||||
resource?.coverUrl,
|
||||
resource?.cover,
|
||||
_staticChatBubbleUrl(resource?.sourceResourceUrl),
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstNonBlankChatBubbleUrl(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
String? _staticChatBubbleUrl(String? value) {
|
||||
final text = value?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return SCNetworkSvgaWidget.isSvga(text) ? null : text;
|
||||
}
|
||||
|
||||
class SCMessageChatPage extends StatefulWidget {
|
||||
final V2TimConversation? conversation;
|
||||
final bool shrinkWrap;
|
||||
@ -101,6 +131,8 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
///互相关注才能互相发送消息
|
||||
bool canSendMsg = false;
|
||||
SocialChatUserProfile? friend;
|
||||
SCVipResourceRes? _selfVipChatBubble;
|
||||
bool _isSelfVipChatBubbleLoading = false;
|
||||
|
||||
///是否显示发送按钮
|
||||
bool showSend = false;
|
||||
@ -150,6 +182,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
rtmProvider?.onRevokeMessageListener = _onRevokeMessage;
|
||||
rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage;
|
||||
loadMsg();
|
||||
_loadSelfVipChatBubble();
|
||||
|
||||
if (_isC2CConversation && friend == null) {
|
||||
loadFriend();
|
||||
@ -337,6 +370,183 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSelfVipChatBubble() async {
|
||||
if (_isSelfVipChatBubbleLoading) return;
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if (currentUserId == null || currentUserId.isEmpty) return;
|
||||
|
||||
_isSelfVipChatBubbleLoading = true;
|
||||
final repository = SCVipRepositoryImp();
|
||||
SCVipHomeRes? vipHome;
|
||||
SCVipResourceRes? chatBubble;
|
||||
|
||||
try {
|
||||
final home = await repository.vipHome();
|
||||
vipHome = home;
|
||||
chatBubble = _resolveSelfVipChatBubbleFromHome(home);
|
||||
debugPrint(
|
||||
'[VIP][ChatBubble] home active=${home.state?.active} '
|
||||
'level=${home.state?.level} levelCode=${home.state?.levelCode} '
|
||||
'selected=${_vipChatBubbleLogValue(chatBubble)}',
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('[VIP][ChatBubble] home load failed error=$error');
|
||||
}
|
||||
|
||||
if (chatBubble == null) {
|
||||
try {
|
||||
final status = await repository.vipStatus();
|
||||
chatBubble = _resolveSelfVipChatBubbleFromStatus(status);
|
||||
if (chatBubble == null &&
|
||||
vipHome != null &&
|
||||
_isUsableVipStatus(status)) {
|
||||
chatBubble = _firstUsableVipChatBubble([
|
||||
_currentVipLevelConfig(
|
||||
vipHome,
|
||||
targetLevel: status.levelInt,
|
||||
targetLevelCode: status.levelCode,
|
||||
)?.chatBubble,
|
||||
]);
|
||||
}
|
||||
debugPrint(
|
||||
'[VIP][ChatBubble] status active=${status.active} '
|
||||
'level=${status.level} levelCode=${status.levelCode} '
|
||||
'selected=${_vipChatBubbleLogValue(chatBubble)}',
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('[VIP][ChatBubble] status load failed error=$error');
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_selfVipChatBubble = chatBubble;
|
||||
_isSelfVipChatBubbleLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromHome(SCVipHomeRes home) {
|
||||
final status = home.state;
|
||||
if (status?.active == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final config = _currentVipLevelConfig(home);
|
||||
if (_isUsableVipStatus(status)) {
|
||||
return _firstUsableVipChatBubble([
|
||||
status?.chatBubble,
|
||||
config?.chatBubble,
|
||||
]);
|
||||
}
|
||||
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
if (profileLevel > 0) {
|
||||
return _firstUsableVipChatBubble([config?.chatBubble]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromStatus(SCVipStatusRes status) {
|
||||
if (!_isUsableVipStatus(status)) {
|
||||
return null;
|
||||
}
|
||||
return _firstUsableVipChatBubble([status.chatBubble]);
|
||||
}
|
||||
|
||||
SCVipLevelConfigRes? _currentVipLevelConfig(
|
||||
SCVipHomeRes home, {
|
||||
int? targetLevel,
|
||||
String? targetLevelCode,
|
||||
}) {
|
||||
final status = home.state;
|
||||
final statusLevel = _vipLevelIntFromStatus(status);
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
final resolvedTargetLevel =
|
||||
(targetLevel ?? 0) > 0
|
||||
? targetLevel!
|
||||
: (statusLevel > 0 ? statusLevel : profileLevel);
|
||||
final statusLevelCode =
|
||||
targetLevelCode?.trim() ?? status?.levelCode?.trim();
|
||||
|
||||
for (final level in home.levels) {
|
||||
if (resolvedTargetLevel > 0 && level.levelInt == resolvedTargetLevel) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
if (statusLevelCode != null && statusLevelCode.isNotEmpty) {
|
||||
for (final level in home.levels) {
|
||||
if (_sameVipLevelCode(level.levelCode, statusLevelCode)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isUsableVipStatus(SCVipStatusRes? status) {
|
||||
return status != null &&
|
||||
status.active != false &&
|
||||
(_vipLevelIntFromStatus(status) > 0 ||
|
||||
_hasNonBlankText(status.levelCode) ||
|
||||
_hasNonBlankText(status.displayName) ||
|
||||
_resolveVipChatBubbleImageUrl(status.chatBubble).isNotEmpty);
|
||||
}
|
||||
|
||||
int _vipLevelIntFromStatus(SCVipStatusRes? status) {
|
||||
final explicitLevel = status?.levelInt ?? 0;
|
||||
if (explicitLevel > 0) {
|
||||
return explicitLevel;
|
||||
}
|
||||
return _parseVipLevelText(status?.levelCode) ??
|
||||
_parseVipLevelText(status?.displayName) ??
|
||||
0;
|
||||
}
|
||||
|
||||
int? _parseVipLevelText(String? value) {
|
||||
final text = value?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final match = RegExp(r'(?:S?VIP)?\s*([1-9]\d*)', caseSensitive: false)
|
||||
.firstMatch(text);
|
||||
return int.tryParse(match?.group(1) ?? '');
|
||||
}
|
||||
|
||||
bool _sameVipLevelCode(String? left, String? right) {
|
||||
final normalizedLeft = left?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
final normalizedRight = right?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
return normalizedLeft != null &&
|
||||
normalizedLeft.isNotEmpty &&
|
||||
normalizedLeft == normalizedRight;
|
||||
}
|
||||
|
||||
bool _hasNonBlankText(String? value) {
|
||||
return value?.trim().isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _firstUsableVipChatBubble(
|
||||
Iterable<SCVipResourceRes?> resources,
|
||||
) {
|
||||
for (final resource in resources) {
|
||||
if (_resolveVipChatBubbleImageUrl(resource).isNotEmpty) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _vipChatBubbleLogValue(SCVipResourceRes? resource) {
|
||||
if (resource == null) return 'null';
|
||||
return '{id:${resource.resourceId},name:${resource.name},'
|
||||
'previewUrl:${resource.previewUrl},'
|
||||
'sourceUrl:${resource.sourceResourceUrl},'
|
||||
'selectedUrl:${_resolveVipChatBubbleImageUrl(resource)}}';
|
||||
}
|
||||
|
||||
///消息列表
|
||||
Widget _msgList() {
|
||||
print('xiaoxiliebiao:${currentConversationMessageList.length}');
|
||||
@ -385,6 +595,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(c, i) => _MessageItem(
|
||||
message: currentConversationMessageList[i],
|
||||
selfVipChatBubble: _selfVipChatBubble,
|
||||
preMessage:
|
||||
i < currentConversationMessageList.length - 1
|
||||
? currentConversationMessageList[i + 1]
|
||||
@ -868,6 +1079,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
class _MessageItem extends StatelessWidget {
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
final SCVipResourceRes? selfVipChatBubble;
|
||||
|
||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||
|
||||
@ -882,6 +1094,7 @@ class _MessageItem extends StatelessWidget {
|
||||
|
||||
_MessageItem({
|
||||
required this.message,
|
||||
this.selfVipChatBubble,
|
||||
this.preMessage,
|
||||
// this.userProfile,
|
||||
this.isSystem = false,
|
||||
@ -1259,14 +1472,46 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTextBubble(String content) {
|
||||
final chatBubble = _senderProfile()?.getChatBox();
|
||||
final chatBubble =
|
||||
(message.isSelf ?? false)
|
||||
? _currentUserChatBubble()
|
||||
: _senderProfile()?.getChatBox();
|
||||
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
|
||||
if (chatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
|
||||
}
|
||||
if (message.isSelf ?? false) {
|
||||
final vipChatBubbleUrl = _resolveVipChatBubbleImageUrl(selfVipChatBubble);
|
||||
if (vipChatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(
|
||||
content: content,
|
||||
imageUrl: vipChatBubbleUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
return _buildDefaultTextBubble(content);
|
||||
}
|
||||
|
||||
PropsResources? _currentUserChatBubble() {
|
||||
final memoryBubble =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getChatBox();
|
||||
if (_resolveChatBubbleImageUrl(memoryBubble).isNotEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
|
||||
final userJson = DataPersistence.getCurrentUser();
|
||||
if (userJson.isEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
try {
|
||||
return SocialChatLoginRes.fromJson(
|
||||
jsonDecode(userJson),
|
||||
).userProfile?.getChatBox();
|
||||
} catch (_) {
|
||||
return memoryBubble;
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _senderProfile() {
|
||||
if (message.isSelf ?? false) {
|
||||
return AccountStorage().getCurrentUser()?.userProfile;
|
||||
@ -1354,7 +1599,7 @@ class _MessageItem extends StatelessWidget {
|
||||
url: imageUrl,
|
||||
width: bubbleWidth,
|
||||
height: bubbleHeight,
|
||||
fallback: _buildVipBubbleFallback(),
|
||||
fallback: SizedBox(width: bubbleWidth, height: bubbleHeight),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
|
||||
@ -1389,11 +1634,11 @@ class _MessageItem extends StatelessWidget {
|
||||
);
|
||||
final bubbleWidth = math.min(
|
||||
maxBubbleWidth,
|
||||
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
|
||||
math.max(minSkinWidth, textSize.width + horizontalPadding * 2),
|
||||
);
|
||||
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
|
||||
final bubbleHeight = math.max(
|
||||
verticalPadding * 2,
|
||||
minSkinHeight,
|
||||
textSize.height + verticalPadding * 2,
|
||||
);
|
||||
final skinAlignment =
|
||||
@ -1408,7 +1653,6 @@ class _MessageItem extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(child: _buildVipBubbleFallback()),
|
||||
Positioned.fill(
|
||||
child: _buildVipBubbleSkin(
|
||||
url: imageUrl,
|
||||
@ -1456,16 +1700,10 @@ class _MessageItem extends StatelessWidget {
|
||||
fit: BoxFit.fill,
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
)
|
||||
: SCNinePatchImage(
|
||||
: _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
),
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
),
|
||||
);
|
||||
@ -1486,15 +1724,44 @@ class _MessageItem extends StatelessWidget {
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
return SCNinePatchImage(
|
||||
return _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStaticVipBubbleImage({
|
||||
required String url,
|
||||
required double width,
|
||||
required double height,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final imageUrl = url.trim();
|
||||
if (imageUrl.isEmpty) {
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
}
|
||||
|
||||
return Image(
|
||||
image: buildCachedImageProvider(
|
||||
imageUrl,
|
||||
logicalWidth: width,
|
||||
logicalHeight: height,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint(
|
||||
'[VIP][ChatBubble] image load failed url=$imageUrl error=$error',
|
||||
);
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Size _measureTextSize({
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
@ -1507,24 +1774,18 @@ class _MessageItem extends StatelessWidget {
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
Widget _buildVipBubbleFallback() {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
resource?.imSendCoverUrl,
|
||||
resource?.imOpenedUrl,
|
||||
resource?.imOpenedUrlTwo,
|
||||
resource?.imNotOpenedUrl,
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.roomNotOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.expand,
|
||||
resource?.sourceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -1452,22 +1452,36 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
if (coins > 9999) {
|
||||
var fMsg = SCFloatingMessage(
|
||||
type: 1,
|
||||
userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||||
toUserId: u.user?.id ?? "",
|
||||
userAvatarUrl:
|
||||
AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "",
|
||||
userName:
|
||||
AccountStorage().getCurrentUser()?.userProfile?.userNickname ??
|
||||
"",
|
||||
toUserName: u.user?.userNickname ?? "",
|
||||
toUserAvatarUrl: u.user?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
coins: coins,
|
||||
number: quantity,
|
||||
);
|
||||
OverlayManager().addMessage(fMsg);
|
||||
_enqueueGiftFloatingMessage(
|
||||
fMsg,
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: fMsg.roomId,
|
||||
senderId: fMsg.userId,
|
||||
receiverId: fMsg.toUserId,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (gift.giftSourceUrl != null && gift.special != null) {
|
||||
if (scGiftHasFullScreenEffect(gift.special)) {
|
||||
if (SCGlobalConfig.isGiftSpecialEffects &&
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_giftFxLog(
|
||||
'local trigger player play '
|
||||
@ -1477,6 +1491,14 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'animationCount=$animationCount',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_enqueueLowPerformanceGiftCompensation(
|
||||
gift: gift,
|
||||
receiver: u.user,
|
||||
quantity: quantity,
|
||||
coins: coins,
|
||||
);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because visual effects disabled '
|
||||
@ -1503,6 +1525,71 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _enqueueLowPerformanceGiftCompensation({
|
||||
required SocialChatGiftRes gift,
|
||||
required SocialChatUserProfile? receiver,
|
||||
required int quantity,
|
||||
required num coins,
|
||||
}) {
|
||||
final sender = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
priority: 900,
|
||||
userId: sender?.id ?? "",
|
||||
toUserId: receiver?.id ?? "",
|
||||
userAvatarUrl: sender?.userAvatar ?? "",
|
||||
toUserAvatarUrl: receiver?.userAvatar ?? "",
|
||||
userName: sender?.userNickname ?? "",
|
||||
toUserName: receiver?.userNickname ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
senderId: sender?.id,
|
||||
receiverId: receiver?.id,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
message,
|
||||
dedupKey: dedupKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(message);
|
||||
}
|
||||
|
||||
String _giftFloatingDedupKey({
|
||||
required String? roomId,
|
||||
required String? senderId,
|
||||
required String? receiverId,
|
||||
required String? giftId,
|
||||
required int quantity,
|
||||
}) {
|
||||
return [
|
||||
"gift",
|
||||
roomId ?? "",
|
||||
senderId ?? "",
|
||||
receiverId ?? "",
|
||||
giftId ?? "",
|
||||
quantity,
|
||||
].join("|");
|
||||
}
|
||||
|
||||
bool _supportsComboFeedback(SocialChatGiftRes gift) {
|
||||
final giftTab = (gift.giftTab ?? '').trim();
|
||||
return giftTab == "LUCK" ||
|
||||
|
||||
@ -18,6 +18,7 @@ import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||||
import '../../shared/tools/sc_entry_popup_coordinator.dart';
|
||||
import '../../shared/tools/sc_lk_event_bus.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||||
@ -34,7 +35,7 @@ class SCIndexPage extends StatefulWidget {
|
||||
State<SCIndexPage> createState() => _SCIndexPageState();
|
||||
}
|
||||
|
||||
class _SCIndexPageState extends State<SCIndexPage> {
|
||||
class _SCIndexPageState extends State<SCIndexPage> with WidgetsBindingObserver {
|
||||
static const Duration _registerRewardSocketWaitTimeout = Duration(seconds: 6);
|
||||
|
||||
int _currentIndex = 0;
|
||||
@ -53,6 +54,7 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_registerRewardSubscription = eventBus
|
||||
.on<RegisterRewardGrantedEvent>()
|
||||
.listen(_onRegisterRewardGranted);
|
||||
@ -93,11 +95,22 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_registerRewardSubscription?.cancel();
|
||||
WakelockPlus.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_showEntryPopupIfNeeded());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@ -363,6 +376,11 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
await _showEntryPopupIfNeeded();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dialogData = await _loadDailySignInDialogData();
|
||||
if (!mounted || dialogData == null) {
|
||||
debugPrint(
|
||||
@ -388,6 +406,16 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showEntryPopupIfNeeded() async {
|
||||
if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||
return;
|
||||
}
|
||||
await SCEntryPopupCoordinator.showIfNeeded(
|
||||
context,
|
||||
onSelectRootTab: _switchBottomTab,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
||||
if (!DataPersistence.getAwaitRegisterRewardSocket() ||
|
||||
DataPersistence.getPendingRegisterRewardDialog()) {
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:yumi/modules/admin/search/sc_edit_room_search_admin_page.dart';
|
||||
import 'package:yumi/modules/admin/search/sc_edit_user_search_admin_page.dart';
|
||||
import 'package:yumi/modules/user/profile/person_detail_page.dart';
|
||||
import 'package:yumi/modules/user/profile/profile_gift_wall_detail_page.dart';
|
||||
import 'package:yumi/modules/user/profile/profile_wall_of_honors_page.dart';
|
||||
import 'package:yumi/modules/search/sc_search_page.dart';
|
||||
import 'package:yumi/modules/media/image_preview_page.dart';
|
||||
import 'package:yumi/modules/media/video_player_page.dart';
|
||||
@ -28,6 +29,7 @@ class SCMainRoute implements SCIRouterProvider {
|
||||
static String report = '/main/report';
|
||||
static String person = '/main/person';
|
||||
static String giftWall = '/main/person/giftWall';
|
||||
static String wallOfHonors = '/main/person/wallOfHonors';
|
||||
static String edit = '/main/person/edit';
|
||||
static String follow = '/main/me/follow';
|
||||
static String vistors = '/main/me/vistors';
|
||||
@ -73,6 +75,14 @@ class SCMainRoute implements SCIRouterProvider {
|
||||
ProfileGiftWallDetailPage(tageId: params['tageId']!.first),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
wallOfHonors,
|
||||
handler: Handler(
|
||||
handlerFunc:
|
||||
(_, params) =>
|
||||
ProfileWallOfHonorsPage(tageId: params['tageId']!.first),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
mainSearch,
|
||||
handler: Handler(handlerFunc: (_, params) => SearchPage()),
|
||||
|
||||
@ -9,10 +9,10 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
|
||||
@ -94,10 +94,10 @@ class _RoomMemberPageState
|
||||
list.isNotEmpty ? buildTag(list.first.roles) : Container(),
|
||||
SizedBox(height: 3.w),
|
||||
Column(
|
||||
children: List.generate(list.length, (cIndex) {
|
||||
SocialChatRoomMemberRes userInfo = list[cIndex];
|
||||
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||
return GestureDetector(
|
||||
children: List.generate(list.length, (cIndex) {
|
||||
SocialChatRoomMemberRes userInfo = list[cIndex];
|
||||
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
||||
@ -108,9 +108,10 @@ class _RoomMemberPageState
|
||||
child: Row(
|
||||
children: [
|
||||
head(
|
||||
url: userProfile?.userAvatar ?? "",
|
||||
width: 52.w,
|
||||
headdress: userProfile?.getHeaddress()?.sourceUrl,
|
||||
url: userProfile?.userAvatar ?? "",
|
||||
width: 52.w,
|
||||
headdress: userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Column(
|
||||
@ -130,47 +131,44 @@ class _RoomMemberPageState
|
||||
SizedBox(width: 3.w),
|
||||
socialchatNickNameText(
|
||||
maxWidth: 160.w,
|
||||
userProfile?.userNickname ?? "",
|
||||
userProfile?.userNickname ?? "",
|
||||
fontSize: 14.sp,
|
||||
textColor: Colors.black,
|
||||
type:
|
||||
userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(userProfile
|
||||
?.userNickname
|
||||
?.characters
|
||||
.length ??
|
||||
0) >
|
||||
type: userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(userProfile
|
||||
?.userNickname
|
||||
?.characters
|
||||
.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: userProfile?.getVIP()?.cover ?? "",
|
||||
userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 5.w),
|
||||
userProfile?.wearBadge?.isNotEmpty ??
|
||||
false
|
||||
userProfile?.wearBadge?.isNotEmpty ?? false
|
||||
? SizedBox(
|
||||
height: 25.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
itemCount:
|
||||
userProfile?.wearBadge?.length ??
|
||||
0,
|
||||
userProfile?.wearBadge?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
return netImage(
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
url:
|
||||
userProfile
|
||||
?.wearBadge?[index]
|
||||
.selectUrl ??
|
||||
"",
|
||||
userProfile
|
||||
?.wearBadge?[index]
|
||||
.selectUrl ??
|
||||
"",
|
||||
);
|
||||
},
|
||||
separatorBuilder: (
|
||||
@ -188,17 +186,16 @@ class _RoomMemberPageState
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SCSpecialIdBadge(
|
||||
idText: userProfile?.getID() ?? "",
|
||||
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||
idText: userProfile?.getID() ?? "",
|
||||
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||
assetPath: SCSpecialIdAssets.userId,
|
||||
animationWidth: 62.w,
|
||||
animationHeight: 24.w,
|
||||
showTextBesideAnimated: true,
|
||||
animatedTextSpacing: 0,
|
||||
showAnimatedGradientText:
|
||||
userProfile
|
||||
?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
userProfile?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
animationTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
@ -352,29 +349,27 @@ class _RoomMemberPageState
|
||||
if (onErr != null) {
|
||||
onErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _mergeCurrentUserVip(
|
||||
SocialChatUserProfile? profile,
|
||||
) {
|
||||
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||
return profile;
|
||||
}
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentProfile == null ||
|
||||
currentProfile.vipLevelForColoredId <= 0 ||
|
||||
currentProfile.id != profile.id) {
|
||||
return profile;
|
||||
}
|
||||
return profile.copyWith(
|
||||
vipLevel:
|
||||
currentProfile.vipLevel ??
|
||||
currentProfile.vipLevelForColoredId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTag(String? roles) {
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _mergeCurrentUserVip(SocialChatUserProfile? profile) {
|
||||
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||
return profile;
|
||||
}
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentProfile == null ||
|
||||
currentProfile.vipLevelForColoredId <= 0 ||
|
||||
currentProfile.id != profile.id) {
|
||||
return profile;
|
||||
}
|
||||
return profile.copyWith(
|
||||
vipLevel:
|
||||
currentProfile.vipLevel ??
|
||||
currentProfile.vipLevelForColoredId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTag(String? roles) {
|
||||
if (roles == SCRoomRolesType.HOMEOWNER.name) {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@ -101,6 +101,7 @@ class _RoomOnlinePageState
|
||||
url: userInfo.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: userInfo.getHeaddress()?.sourceUrl,
|
||||
headdressCover: userInfo.getHeaddress()?.cover,
|
||||
),
|
||||
onTap: () {
|
||||
_openUserCard(userInfo);
|
||||
|
||||
@ -97,6 +97,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
width: seatAvatarSize,
|
||||
height: seatAvatarSize,
|
||||
headdress: resolvedHeaddress,
|
||||
headdressCover: seatSnapshot.headdressCoverUrl,
|
||||
),
|
||||
)
|
||||
: (seatSnapshot.micLock
|
||||
@ -327,6 +328,7 @@ class _SeatRenderSnapshot {
|
||||
required this.userId,
|
||||
required this.userAvatar,
|
||||
required this.headdressSourceUrl,
|
||||
required this.headdressCoverUrl,
|
||||
required this.userNickname,
|
||||
required this.userRoles,
|
||||
required this.userVipName,
|
||||
@ -349,6 +351,7 @@ class _SeatRenderSnapshot {
|
||||
userId: user?.id ?? "",
|
||||
userAvatar: user?.userAvatar ?? "",
|
||||
headdressSourceUrl: user?.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCoverUrl: user?.getHeaddress()?.cover ?? "",
|
||||
userNickname: user?.userNickname ?? "",
|
||||
userRoles: user?.roles ?? "",
|
||||
userVipName: user?.getVIP()?.name ?? "",
|
||||
@ -365,6 +368,7 @@ class _SeatRenderSnapshot {
|
||||
final String userId;
|
||||
final String userAvatar;
|
||||
final String headdressSourceUrl;
|
||||
final String headdressCoverUrl;
|
||||
final String userNickname;
|
||||
final String userRoles;
|
||||
final String userVipName;
|
||||
@ -388,6 +392,7 @@ class _SeatRenderSnapshot {
|
||||
other.userId == userId &&
|
||||
other.userAvatar == userAvatar &&
|
||||
other.headdressSourceUrl == headdressSourceUrl &&
|
||||
other.headdressCoverUrl == headdressCoverUrl &&
|
||||
other.userNickname == userNickname &&
|
||||
other.userRoles == userRoles &&
|
||||
other.userVipName == userVipName &&
|
||||
@ -405,6 +410,7 @@ class _SeatRenderSnapshot {
|
||||
userId,
|
||||
userAvatar,
|
||||
headdressSourceUrl,
|
||||
headdressCoverUrl,
|
||||
userNickname,
|
||||
userRoles,
|
||||
userVipName,
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/modules/room_game/data/room_game_repository.dart';
|
||||
import 'package:yumi/modules/room_game/utils/room_game_viewport.dart';
|
||||
@ -14,9 +17,14 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RoomGameListSheet extends StatefulWidget {
|
||||
const RoomGameListSheet({super.key, required this.roomContext});
|
||||
const RoomGameListSheet({
|
||||
super.key,
|
||||
required this.roomContext,
|
||||
this.initialGameId,
|
||||
});
|
||||
|
||||
final BuildContext roomContext;
|
||||
final String? initialGameId;
|
||||
|
||||
@override
|
||||
State<RoomGameListSheet> createState() => _RoomGameListSheetState();
|
||||
@ -35,6 +43,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
|
||||
String _roomId = '';
|
||||
String? _launchingGameId;
|
||||
bool _hasHandledInitialGame = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -53,9 +62,47 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
roomId: _roomId,
|
||||
category: 'CHAT_ROOM',
|
||||
);
|
||||
_scheduleInitialGameOpen(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
void _scheduleInitialGameOpen(List<RoomGameListItemModel> items) {
|
||||
final targetGameId = widget.initialGameId?.trim() ?? '';
|
||||
if (_hasHandledInitialGame || targetGameId.isEmpty || items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_hasHandledInitialGame = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final game = _resolveInitialGame(items, targetGameId);
|
||||
if (game == null) {
|
||||
SCTts.show('Game not found');
|
||||
return;
|
||||
}
|
||||
unawaited(_openGame(game));
|
||||
});
|
||||
}
|
||||
|
||||
RoomGameListItemModel? _resolveInitialGame(
|
||||
List<RoomGameListItemModel> items,
|
||||
String targetGameId,
|
||||
) {
|
||||
final supportedItems =
|
||||
items.where((item) => item.isBaishun || item.isLeader).toList();
|
||||
final candidates = supportedItems.isNotEmpty ? supportedItems : items;
|
||||
if (targetGameId == '0') {
|
||||
return candidates[math.Random().nextInt(candidates.length)];
|
||||
}
|
||||
for (final item in candidates) {
|
||||
if (item.gameId == targetGameId || item.providerGameId == targetGameId) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _retry() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
|
||||
@ -44,7 +44,10 @@ class _FansUserListPageState
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: SocialChatStandardAppBar(title: SCAppLocalizations.of(context)!.fansList, actions: []),
|
||||
appBar: SocialChatStandardAppBar(
|
||||
title: SCAppLocalizations.of(context)!.fansList,
|
||||
actions: [],
|
||||
),
|
||||
body: buildList(context),
|
||||
),
|
||||
],
|
||||
@ -63,6 +66,7 @@ class _FansUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
@ -78,17 +82,17 @@ class _FansUserListPageState
|
||||
textColor: Colors.white,
|
||||
type: res.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
res.userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
|
||||
@ -66,6 +66,7 @@ class _FollowUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
|
||||
@ -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/task/task_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';
|
||||
@ -34,6 +35,7 @@ class _MePage2State extends State<MePage2> {
|
||||
|
||||
Map<String, SCUserCounterRes> _counterMap = {};
|
||||
SCVipStatusRes? _vipStatus;
|
||||
int _taskClaimableCount = 0;
|
||||
bool _isVipStatusLoading = false;
|
||||
|
||||
@override
|
||||
@ -53,6 +55,7 @@ class _MePage2State extends State<MePage2> {
|
||||
profileManager.balance();
|
||||
_loadCounter();
|
||||
_loadVipStatus();
|
||||
_loadTaskClaimableCount();
|
||||
}
|
||||
|
||||
Future<void> _loadCounter() async {
|
||||
@ -98,6 +101,17 @@ class _MePage2State extends State<MePage2> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadTaskClaimableCount() async {
|
||||
try {
|
||||
final count = await SCAccountRepository().taskClaimableCount();
|
||||
if (!mounted) return;
|
||||
setState(() => _taskClaimableCount = count);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _taskClaimableCount = 0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<SCVipStatusRes> _loadVipEntryStatus() async {
|
||||
final repository = SCVipRepositoryImp();
|
||||
try {
|
||||
@ -239,6 +253,7 @@ class _MePage2State extends State<MePage2> {
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
headdress: profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: profile?.getHeaddress()?.cover,
|
||||
showDefault: true,
|
||||
),
|
||||
],
|
||||
@ -320,6 +335,27 @@ class _MePage2State extends State<MePage2> {
|
||||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.task,
|
||||
iconPath: 'sc_images/index/sc_icon_task.png',
|
||||
badgeCount: _taskClaimableCount,
|
||||
onTap: () async {
|
||||
await SCNavigatorUtils.push(context, TaskRoute.task);
|
||||
if (!mounted) return;
|
||||
_loadTaskClaimableCount();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.level,
|
||||
iconPath: 'sc_images/index/sc_icon_level.png',
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.bag,
|
||||
iconPath: 'sc_images/index/sc_icon_bag.png',
|
||||
@ -820,6 +856,7 @@ class _MePage2State extends State<MePage2> {
|
||||
required VoidCallback onTap,
|
||||
double? iconWidth,
|
||||
double? iconHeight,
|
||||
int badgeCount = 0,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: SCDebounceWidget(
|
||||
@ -832,26 +869,33 @@ class _MePage2State extends State<MePage2> {
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
iconPath,
|
||||
width: (iconWidth ?? 54).w,
|
||||
height: (iconHeight ?? 54).w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
iconPath,
|
||||
width: (iconWidth ?? 54).w,
|
||||
height: (iconHeight ?? 54).w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (badgeCount > 0) _buildEntryBadge(badgeCount),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -859,6 +903,35 @@ class _MePage2State extends State<MePage2> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntryBadge(int count) {
|
||||
final text = count > 99 ? '99+' : count.toString();
|
||||
return PositionedDirectional(
|
||||
top: 11.w,
|
||||
end: 14.w,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minWidth: 18.w),
|
||||
height: 18.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.w),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF4B4B),
|
||||
borderRadius: BorderRadius.circular(9.w),
|
||||
border: Border.all(color: Colors.white, width: 1.w),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem({
|
||||
required String value,
|
||||
required String label,
|
||||
|
||||
@ -242,6 +242,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
url: ref.userProfile?.userAvatar ?? "",
|
||||
width: 88.w,
|
||||
headdress: ref.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: ref.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
onTap: () {
|
||||
String encodedUrls = Uri.encodeComponent(
|
||||
@ -632,7 +633,15 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildProfileSectionTitle("Wall Of Honors"),
|
||||
_buildProfileSectionTitle(
|
||||
"Wall Of Honors",
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.wallOfHonors}?tageId=${widget.tageId}",
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
_buildWallGrid(
|
||||
itemCount: 4,
|
||||
@ -668,7 +677,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
? "sc_images/person/sc_icon_profile_wall_of_honors_title.png"
|
||||
: "sc_images/person/sc_icon_profile_gift_wall_title.png";
|
||||
final titleHeight = 32.h;
|
||||
final titleWidth = title == "Wall Of Honors" ? 98.w : 61.w;
|
||||
final titleWidth = title == "Wall Of Honors" ? 98.w : 61.w;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
@ -8,6 +11,8 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
@ -29,6 +34,8 @@ class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
SocialChatUserProfile? _profile;
|
||||
List<SocialChatGiftRes> _gifts = [];
|
||||
bool _loadingGifts = true;
|
||||
String? _playingGiftSvgaUrl;
|
||||
int _giftEffectSerial = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -177,6 +184,33 @@ class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
bool _isSvgaGift(SocialChatGiftRes gift) {
|
||||
final sourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
return sourceUrl.isNotEmpty &&
|
||||
SCPathUtils.getFileExtension(sourceUrl).toLowerCase() == ".svga";
|
||||
}
|
||||
|
||||
void _playGiftSvgaOnce(SocialChatGiftRes gift) {
|
||||
final sourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
if (!_isSvgaGift(gift) || _playingGiftSvgaUrl != null) {
|
||||
return;
|
||||
}
|
||||
SCGiftVapSvgaManager().preload(sourceUrl, highPriority: true);
|
||||
setState(() {
|
||||
_playingGiftSvgaUrl = sourceUrl;
|
||||
_giftEffectSerial++;
|
||||
});
|
||||
}
|
||||
|
||||
void _clearGiftSvgaEffect() {
|
||||
if (!mounted || _playingGiftSvgaUrl == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_playingGiftSvgaUrl = null;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildGradientBorder({
|
||||
double? width,
|
||||
double? height,
|
||||
@ -254,6 +288,7 @@ class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
url: _profile?.userAvatar ?? "",
|
||||
width: 104.w,
|
||||
headdress: _profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: _profile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
socialchatNickNameText(
|
||||
@ -398,50 +433,54 @@ class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
final quantity = _giftQuantity(gift);
|
||||
final giftName =
|
||||
(gift.giftName ?? "").trim().isEmpty ? "--" : gift.giftName!.trim();
|
||||
return _buildGradientBorder(
|
||||
radius: 8.w,
|
||||
backgroundColor: _cardBg,
|
||||
endAlpha: 0.62,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 8.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: netImage(
|
||||
url: gift.giftPhoto ?? "",
|
||||
width: 82.w,
|
||||
height: 74.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
text(
|
||||
"$giftName*$quantity",
|
||||
fontSize: 13,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildCoinIcon(12.w),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: text(
|
||||
_formatNumber(_giftAmount(gift)),
|
||||
fontSize: 11,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _playGiftSvgaOnce(gift),
|
||||
child: _buildGradientBorder(
|
||||
radius: 8.w,
|
||||
backgroundColor: _cardBg,
|
||||
endAlpha: 0.62,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 8.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: netImage(
|
||||
url: gift.giftPhoto ?? "",
|
||||
width: 82.w,
|
||||
height: 74.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
text(
|
||||
"$giftName*$quantity",
|
||||
fontSize: 13,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildCoinIcon(12.w),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: text(
|
||||
_formatNumber(_giftAmount(gift)),
|
||||
fontSize: 11,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -464,31 +503,152 @@ class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom + 22.w;
|
||||
final playingGiftSvgaUrl = _playingGiftSvgaUrl;
|
||||
return Scaffold(
|
||||
backgroundColor: _profileBg,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(children: [_buildTopBar(), _buildProfileHeader()]),
|
||||
),
|
||||
_buildGiftGrid(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 54.w, bottom: bottomPadding),
|
||||
child: Center(
|
||||
child: text(
|
||||
"No Further Data Available~",
|
||||
fontSize: 13,
|
||||
textColor: const Color(0xffB1B1B1),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [_buildTopBar(), _buildProfileHeader()],
|
||||
),
|
||||
),
|
||||
_buildGiftGrid(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 54.w, bottom: bottomPadding),
|
||||
child: Center(
|
||||
child: text(
|
||||
"No Further Data Available~",
|
||||
fontSize: 13,
|
||||
textColor: const Color(0xffB1B1B1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (playingGiftSvgaUrl != null)
|
||||
Positioned.fill(
|
||||
child: _GiftWallSvgaEffectOverlay(
|
||||
key: ValueKey("$playingGiftSvgaUrl-$_giftEffectSerial"),
|
||||
sourceUrl: playingGiftSvgaUrl,
|
||||
onFinished: _clearGiftSvgaEffect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GiftWallSvgaEffectOverlay extends StatefulWidget {
|
||||
const _GiftWallSvgaEffectOverlay({
|
||||
super.key,
|
||||
required this.sourceUrl,
|
||||
required this.onFinished,
|
||||
});
|
||||
|
||||
final String sourceUrl;
|
||||
final VoidCallback onFinished;
|
||||
|
||||
@override
|
||||
State<_GiftWallSvgaEffectOverlay> createState() =>
|
||||
_GiftWallSvgaEffectOverlayState();
|
||||
}
|
||||
|
||||
class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SVGAAnimationController _controller;
|
||||
bool _loaded = false;
|
||||
bool _finished = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SVGAAnimationController(vsync: this);
|
||||
_controller.addStatusListener(_handleStatusChanged);
|
||||
_loadAndPlay();
|
||||
}
|
||||
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_finish();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAndPlay() async {
|
||||
try {
|
||||
final movieEntity = await _loadMovieEntity(widget.sourceUrl);
|
||||
if (!mounted || _finished) return;
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
_controller
|
||||
..reset()
|
||||
..repeat(count: 1);
|
||||
} catch (error) {
|
||||
debugPrint("[GiftWallSVGA] load failed ${widget.sourceUrl}: $error");
|
||||
_finish();
|
||||
}
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadMovieEntity(String sourceUrl) async {
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
final cached = cache[sourceUrl];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final pathType = SCPathUtils.getPathType(sourceUrl);
|
||||
late final MovieEntity movieEntity;
|
||||
if (pathType == PathType.network) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl);
|
||||
} else if (pathType == PathType.asset) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl);
|
||||
} else if (pathType == PathType.file) {
|
||||
final bytes = await File(sourceUrl).readAsBytes();
|
||||
movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes);
|
||||
} else {
|
||||
throw Exception("Unsupported SVGA source: $sourceUrl");
|
||||
}
|
||||
movieEntity.autorelease = false;
|
||||
cache[sourceUrl] = movieEntity;
|
||||
return movieEntity;
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
widget.onFinished();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_handleStatusChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
alignment: Alignment.center,
|
||||
child:
|
||||
_loaded
|
||||
? SVGAImage(
|
||||
_controller,
|
||||
fit: BoxFit.contain,
|
||||
allowDrawingOverflow: false,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
152
lib/modules/user/profile/profile_wall_of_honors_page.dart
Normal file
@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class ProfileWallOfHonorsPage extends StatefulWidget {
|
||||
const ProfileWallOfHonorsPage({super.key, required this.tageId});
|
||||
|
||||
final String tageId;
|
||||
|
||||
@override
|
||||
State<ProfileWallOfHonorsPage> createState() =>
|
||||
_ProfileWallOfHonorsPageState();
|
||||
}
|
||||
|
||||
class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
|
||||
static const Color _profileBg = Color(0xff072121);
|
||||
static const Color _mutedText = Color(0xff8F9A98);
|
||||
|
||||
int _selectedTabIndex = 0;
|
||||
final int _honorCount = 0;
|
||||
final int _eventCount = 0;
|
||||
|
||||
int get _totalCount => _honorCount + _eventCount;
|
||||
|
||||
Widget _buildTopBar() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 54.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 8.w,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_left_rounded,
|
||||
color: Colors.white,
|
||||
size: 38.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"My Achievements($_totalCount)",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabs() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 82.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildTab(index: 0, label: "Honors($_honorCount)")),
|
||||
Expanded(child: _buildTab(index: 1, label: "Event($_eventCount)")),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab({required int index, required String label}) {
|
||||
final selected = _selectedTabIndex == index;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_selectedTabIndex == index) return;
|
||||
setState(() {
|
||||
_selectedTabIndex = index;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
text(
|
||||
label,
|
||||
fontSize: 20,
|
||||
textColor: selected ? Colors.white : _mutedText,
|
||||
fontWeight: FontWeight.w400,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 9.w),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: selected ? 28.w : 0,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
text(
|
||||
"Stay Tuned For More Achievements!",
|
||||
fontSize: 13,
|
||||
textColor: const Color(0xffA4AAA8),
|
||||
fontWeight: FontWeight.w400,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 120.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _profileBg,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(children: [_buildTopBar(), _buildTabs()]),
|
||||
),
|
||||
_buildEmptyState(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -20,10 +20,11 @@ class _TaskPageState extends State<TaskPage> {
|
||||
static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png';
|
||||
static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png';
|
||||
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
||||
static const String _taskDefaultAsset = 'sc_images/index/sc_icon_task.png';
|
||||
|
||||
final SCAccountRepository _repository = SCAccountRepository();
|
||||
final Set<num> _claimedTaskIds = {};
|
||||
final Set<num> _claimingTaskIds = {};
|
||||
final Set<String> _claimedTaskIds = {};
|
||||
final Set<String> _claimingTaskIds = {};
|
||||
List<SCTaskListRes> _tasks = [];
|
||||
bool _loading = true;
|
||||
String? _toastMessage;
|
||||
@ -42,16 +43,26 @@ class _TaskPageState extends State<TaskPage> {
|
||||
}
|
||||
|
||||
Future<void> _loadTasks() async {
|
||||
debugPrint('[TaskCenter][Page] load start');
|
||||
try {
|
||||
final tasks = await _repository.tasks();
|
||||
if (!mounted) return;
|
||||
final sortedTasks =
|
||||
tasks..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
final dailyCount =
|
||||
sortedTasks.where((task) => (task.taskType ?? 0).toInt() == 0).length;
|
||||
final exclusiveCount = sortedTasks.length - dailyCount;
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] load success total=${sortedTasks.length} '
|
||||
'daily=$dailyCount exclusive=$exclusiveCount '
|
||||
'sample=${_taskDebugSample(sortedTasks)}',
|
||||
);
|
||||
setState(() {
|
||||
_tasks =
|
||||
tasks
|
||||
..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
_tasks = sortedTasks;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('[TaskCenter][Page] load failed error=$error');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_tasks = [];
|
||||
@ -184,6 +195,10 @@ class _TaskPageState extends State<TaskPage> {
|
||||
List<_TaskUiItem> dailyTasks,
|
||||
List<_TaskUiItem> newcomerTasks,
|
||||
) {
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] build sections raw=${_tasks.length} '
|
||||
'daily=${dailyTasks.length} exclusive=${newcomerTasks.length}',
|
||||
);
|
||||
final sections = <Widget>[];
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
if (dailyTasks.isNotEmpty) {
|
||||
@ -209,6 +224,21 @@ class _TaskPageState extends State<TaskPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (sections.isEmpty) {
|
||||
sections.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 120.w),
|
||||
child: Text(
|
||||
l10n.noPromptsToday,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
@ -218,21 +248,11 @@ class _TaskPageState extends State<TaskPage> {
|
||||
final taskType = (task.taskType ?? 0).toInt();
|
||||
return isNewcomer ? taskType != 0 : taskType == 0;
|
||||
}).toList();
|
||||
final mapped = source.map(_mapTask).toList();
|
||||
if (_tasks.isNotEmpty) return mapped;
|
||||
final demoTasks = _demoTasks(isNewcomer: isNewcomer);
|
||||
return demoTasks
|
||||
.map(
|
||||
(task) =>
|
||||
_claimedTaskIds.contains(task.id)
|
||||
? task.copyWith(status: _TaskActionStatus.claimed)
|
||||
: task,
|
||||
)
|
||||
.toList();
|
||||
return source.map(_mapTask).toList();
|
||||
}
|
||||
|
||||
_TaskUiItem _mapTask(SCTaskListRes task) {
|
||||
final id = task.taskId ?? -1;
|
||||
final id = task.taskId ?? '';
|
||||
final currentValue = _parseNumber(task.completedValue) ?? 0;
|
||||
final targetValue =
|
||||
_parseNumber(task.targetValue) ??
|
||||
@ -255,16 +275,22 @@ class _TaskPageState extends State<TaskPage> {
|
||||
_nonEmpty(task.taskName) ??
|
||||
SCAppLocalizations.of(context)!.useMicrophoneForOneMin,
|
||||
progressText:
|
||||
_nonEmpty(task.taskDesc) ??
|
||||
'${_formatPlainNumber(currentValue)}/${_formatPlainNumber(targetValue)}',
|
||||
rewardText: _formatCompactNumber(task.quantity ?? 1100),
|
||||
status: status,
|
||||
iconAsset: _iconForTask(task),
|
||||
iconUrl: _nonEmpty(task.taskIcon) ?? _nonEmpty(task.cover),
|
||||
jumpPage: _nonEmpty(task.jumpPage),
|
||||
fromApi: task.taskId != null,
|
||||
fromApi: id.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleTaskAction(_TaskUiItem task) async {
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] action id=${task.id} status=${task.status.name} '
|
||||
'fromApi=${task.fromApi} jumpPage=${task.jumpPage}',
|
||||
);
|
||||
if (task.status == _TaskActionStatus.claimed) return;
|
||||
if (task.status == _TaskActionStatus.go) {
|
||||
_openJumpPage(task.jumpPage);
|
||||
@ -274,7 +300,7 @@ class _TaskPageState extends State<TaskPage> {
|
||||
if (_claimingTaskIds.contains(task.id)) return;
|
||||
setState(() => _claimingTaskIds.add(task.id));
|
||||
var success = true;
|
||||
if (task.fromApi && task.id >= 0) {
|
||||
if (task.fromApi && task.id.isNotEmpty) {
|
||||
try {
|
||||
success = await _repository.taskReward(task.id);
|
||||
} catch (_) {
|
||||
@ -323,7 +349,8 @@ class _TaskPageState extends State<TaskPage> {
|
||||
.toLowerCase();
|
||||
if (text.contains('gift')) return _taskGiftAsset;
|
||||
if (text.contains('game')) return _taskGameAsset;
|
||||
return _taskMicAsset;
|
||||
if (text.contains('mic')) return _taskMicAsset;
|
||||
return _taskDefaultAsset;
|
||||
}
|
||||
|
||||
static num? _parseNumber(Object? value) {
|
||||
@ -358,35 +385,17 @@ class _TaskPageState extends State<TaskPage> {
|
||||
return text.endsWith('.0') ? text.substring(0, text.length - 2) : text;
|
||||
}
|
||||
|
||||
List<_TaskUiItem> _demoTasks({required bool isNewcomer}) {
|
||||
final title = SCAppLocalizations.of(context)!.useMicrophoneForOneMin;
|
||||
final baseId = isNewcomer ? -200 : -100;
|
||||
return [
|
||||
_TaskUiItem(
|
||||
id: baseId - 1,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.go,
|
||||
iconAsset: _taskMicAsset,
|
||||
),
|
||||
_TaskUiItem(
|
||||
id: baseId - 2,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.claim,
|
||||
iconAsset: _taskGameAsset,
|
||||
),
|
||||
_TaskUiItem(
|
||||
id: baseId - 3,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.claimed,
|
||||
iconAsset: _taskGiftAsset,
|
||||
),
|
||||
];
|
||||
String _taskDebugSample(List<SCTaskListRes> tasks) {
|
||||
if (tasks.isEmpty) return 'empty';
|
||||
return tasks
|
||||
.take(3)
|
||||
.map((task) {
|
||||
return '{id=${task.taskId},type=${task.taskType},'
|
||||
'status=${task.taskStatus},collected=${task.isRewardCollected},'
|
||||
'current=${task.completedValue},target=${task.targetValue},'
|
||||
'quantity=${task.quantity},name=${task.taskName}}';
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,7 +412,7 @@ class _TaskSection extends StatelessWidget {
|
||||
final String subtitle;
|
||||
final List<_TaskUiItem> tasks;
|
||||
final ValueChanged<_TaskUiItem> onAction;
|
||||
final bool Function(num id) isClaiming;
|
||||
final bool Function(String id) isClaiming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -564,12 +573,7 @@ class _TaskRow extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
task.iconAsset,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
child: _TaskIcon(task: task),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Expanded(
|
||||
@ -697,6 +701,40 @@ class _TaskActionButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskIcon extends StatelessWidget {
|
||||
const _TaskIcon({required this.task});
|
||||
|
||||
final _TaskUiItem task;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconUrl = task.iconUrl?.trim();
|
||||
if (iconUrl == null || iconUrl.isEmpty) {
|
||||
return _buildAssetIcon();
|
||||
}
|
||||
return Image.network(
|
||||
iconUrl,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _buildAssetIcon(),
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return _buildAssetIcon();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetIcon() {
|
||||
return Image.asset(
|
||||
task.iconAsset,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskButtonConfig {
|
||||
const _TaskButtonConfig({
|
||||
required this.label,
|
||||
@ -758,16 +796,18 @@ class _TaskUiItem {
|
||||
required this.rewardText,
|
||||
required this.status,
|
||||
required this.iconAsset,
|
||||
this.iconUrl,
|
||||
this.jumpPage,
|
||||
this.fromApi = false,
|
||||
});
|
||||
|
||||
final num id;
|
||||
final String id;
|
||||
final String title;
|
||||
final String progressText;
|
||||
final String rewardText;
|
||||
final _TaskActionStatus status;
|
||||
final String iconAsset;
|
||||
final String? iconUrl;
|
||||
final String? jumpPage;
|
||||
final bool fromApi;
|
||||
|
||||
@ -779,6 +819,7 @@ class _TaskUiItem {
|
||||
rewardText: rewardText,
|
||||
status: status ?? this.status,
|
||||
iconAsset: iconAsset,
|
||||
iconUrl: iconUrl,
|
||||
jumpPage: jumpPage,
|
||||
fromApi: fromApi,
|
||||
);
|
||||
|
||||
@ -64,6 +64,7 @@ class _VisitorUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
@ -79,17 +80,17 @@ class _VisitorUserListPageState
|
||||
textColor: Colors.white,
|
||||
type: res.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
res.userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
|
||||
@ -8,6 +8,8 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
@ -25,14 +27,16 @@ import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'
|
||||
hide PropsResources;
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'
|
||||
as follow_room;
|
||||
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'
|
||||
as my_room;
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||
hide PropsResources;
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
|
||||
@ -41,6 +45,7 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||||
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||
@ -319,6 +324,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _joinAgoraTimeoutTimer;
|
||||
Timer? _agoraDisconnectedCleanupTimer;
|
||||
Timer? _roomEntryEffectTimer;
|
||||
Timer? _roomRedPacketPresenceTimer;
|
||||
Completer<void>? _joinAgoraCompleter;
|
||||
Future<RtcEngine>? _rtcEngineInitTask;
|
||||
Future<void>? _rtcEnginePrewarmTask;
|
||||
@ -2638,6 +2644,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
await _bootstrapRoomHeartbeatState();
|
||||
_startRoomRedPacketPresenceHeartbeat(roomId);
|
||||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||||
return;
|
||||
}
|
||||
@ -2667,20 +2674,68 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String _firstNonBlankEntryResource(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
String _resolveEntryCompensationCover(PropsResources? resource) {
|
||||
return _firstNonBlankEntryResource([
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.roomNotOpenedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
void _enqueueLowPerformanceEntryCompensation({
|
||||
required Msg joinMsg,
|
||||
required PropsResources resource,
|
||||
required String roomId,
|
||||
}) {
|
||||
if (!SCGlobalConfig.isLowPerformanceDevice ||
|
||||
!shouldShowRoomVisualEffects ||
|
||||
roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final coverUrl = _resolveEntryCompensationCover(resource);
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage(
|
||||
type: 5,
|
||||
priority: 950,
|
||||
userId: joinMsg.user?.id ?? "",
|
||||
roomId: roomId,
|
||||
userAvatarUrl: joinMsg.user?.userAvatar ?? "",
|
||||
userName: joinMsg.user?.userNickname ?? "",
|
||||
giftUrl: coverUrl,
|
||||
fallbackUrl: coverUrl,
|
||||
),
|
||||
dedupKey: [
|
||||
"entry",
|
||||
roomId,
|
||||
joinMsg.user?.id ?? "",
|
||||
resource.id ?? "",
|
||||
].join("|"),
|
||||
);
|
||||
}
|
||||
|
||||
void _playRoomEntryEffects(
|
||||
Msg joinMsg, {
|
||||
required int entryRequestSerial,
|
||||
required String roomId,
|
||||
}) {
|
||||
if (SCGlobalConfig.isEntryVehicleAnimation) {
|
||||
final sourceUrl =
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.getMountains()
|
||||
?.sourceUrl ??
|
||||
"";
|
||||
final entryResource =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getMountains();
|
||||
if (entryResource != null) {
|
||||
final sourceUrl = entryResource.sourceUrl ?? "";
|
||||
if (sourceUrl.isNotEmpty &&
|
||||
SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects &&
|
||||
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
@ -2696,6 +2751,23 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
});
|
||||
} else if (sourceUrl.isNotEmpty &&
|
||||
SCGlobalConfig.isLowPerformanceDevice &&
|
||||
shouldShowRoomVisualEffects &&
|
||||
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () {
|
||||
_roomEntryEffectTimer = null;
|
||||
if (!shouldShowRoomVisualEffects ||
|
||||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||
return;
|
||||
}
|
||||
_enqueueLowPerformanceEntryCompensation(
|
||||
joinMsg: joinMsg,
|
||||
resource: entryResource,
|
||||
roomId: roomId,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (rtmProvider?.msgUserJoinListener != null) {
|
||||
@ -2849,6 +2921,37 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
void _startRoomRedPacketPresenceHeartbeat(String roomId) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
void sendHeartbeat() {
|
||||
if (!_isCurrentRoomId(normalizedRoomId)) {
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
return;
|
||||
}
|
||||
SCChatRoomRepository()
|
||||
.roomRedPacketPresenceHeartbeat(normalizedRoomId)
|
||||
.catchError((e) {
|
||||
debugPrint('[RoomRedPacket][Presence] heartbeat failed: $e');
|
||||
return SCRoomRedPacketPresenceRes();
|
||||
});
|
||||
}
|
||||
|
||||
sendHeartbeat();
|
||||
_roomRedPacketPresenceTimer = Timer.periodic(
|
||||
const Duration(seconds: 30),
|
||||
(_) => sendHeartbeat(),
|
||||
);
|
||||
}
|
||||
|
||||
void _stopRoomRedPacketPresenceHeartbeat() {
|
||||
_roomRedPacketPresenceTimer?.cancel();
|
||||
_roomRedPacketPresenceTimer = null;
|
||||
}
|
||||
|
||||
Future exitCurrentVoiceRoomSession(
|
||||
bool isLogout, {
|
||||
bool deferRemoteCleanup = true,
|
||||
@ -2870,6 +2973,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
setRoomVisualEffectsEnabled(false);
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
exitRtmProvider?.cleanRoomData();
|
||||
} catch (e) {
|
||||
debugPrint('rtc退出房间本地清理出错: $e');
|
||||
@ -2912,6 +3016,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
setRoomVisualEffectsEnabled(false);
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
exitRtmProvider?.cleanRoomData();
|
||||
} catch (e) {
|
||||
debugPrint('rtc切换房间本地清理出错: $e');
|
||||
@ -2971,6 +3076,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
if (roomId.isNotEmpty) {
|
||||
tasks.add(
|
||||
_retryExitNetworkRequest(
|
||||
'leave room red packet presence $roomId',
|
||||
() async {
|
||||
await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId);
|
||||
},
|
||||
),
|
||||
);
|
||||
tasks.add(
|
||||
_retryExitNetworkRequest('quit room $roomId', () async {
|
||||
if (_isCurrentRoomId(roomId)) {
|
||||
@ -3065,6 +3178,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
rtmProvider?.cleanRoomData();
|
||||
if (!isPreviewOnlyFailure && groupId.isNotEmpty) {
|
||||
await rtmProvider?.quitGroup(groupId);
|
||||
@ -3072,6 +3186,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
await _cleanupAgoraRoomState(clearMicSeats: true);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (!isPreviewOnlyFailure && roomId.isNotEmpty) {
|
||||
await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId);
|
||||
await SCAccountRepository().quitRoom(roomId);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -3091,6 +3206,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomEntryRequestSerial += 1;
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = null;
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
_previewRoomSeatCount = null;
|
||||
_finishRoomStartupSeatLoading(executePendingAction: false);
|
||||
_resetSelfMicGoUpFailureState();
|
||||
|
||||
@ -45,6 +45,7 @@ import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart';
|
||||
@ -85,6 +86,64 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
debugPrint('[GiftFX][RTM] $message');
|
||||
}
|
||||
|
||||
void _enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
message,
|
||||
dedupKey: dedupKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(message);
|
||||
}
|
||||
|
||||
String _giftFloatingDedupKey({
|
||||
required String? roomId,
|
||||
required String? senderId,
|
||||
required String? receiverId,
|
||||
required String? giftId,
|
||||
required num? quantity,
|
||||
}) {
|
||||
return [
|
||||
"gift",
|
||||
roomId ?? "",
|
||||
senderId ?? "",
|
||||
receiverId ?? "",
|
||||
giftId ?? "",
|
||||
quantity ?? 0,
|
||||
].join("|");
|
||||
}
|
||||
|
||||
String _entryFloatingDedupKey({
|
||||
required String? roomId,
|
||||
required String? userId,
|
||||
required String? resourceId,
|
||||
}) {
|
||||
return ["entry", roomId ?? "", userId ?? "", resourceId ?? ""].join("|");
|
||||
}
|
||||
|
||||
String _resolveEntryCompensationCover(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.roomNotOpenedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
///消息列表
|
||||
List<Msg> roomAllMsgList = [];
|
||||
List<Msg> roomChatMsgList = [];
|
||||
@ -126,6 +185,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
SCBroadCastLuckGiftPush? currentPlayingLuckGift;
|
||||
final Queue<SCBroadCastLuckGiftPush?> _luckGiftPushQueue = Queue();
|
||||
String? _currentLuckGiftPushKey;
|
||||
String? roomRedPacketBroadcastGroupId;
|
||||
String? roomRedPacketBroadcastRegionCode;
|
||||
Debouncer debouncer = Debouncer();
|
||||
List<V2TimConversation> conversationList = [];
|
||||
|
||||
@ -380,6 +441,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
getAllUnReadCount();
|
||||
joinBigBroadcastGroup();
|
||||
unawaited(syncRoomRedPacketBroadcastGroup());
|
||||
}
|
||||
|
||||
/// 初始化会话
|
||||
@ -491,9 +553,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
_onNewMessage(V2TimMessage message) {
|
||||
if (message.groupID != null) {
|
||||
///全服通知
|
||||
if (message.groupID == SCGlobalConfig.bigBroadcastGroup) {
|
||||
///全服通知 / 语区红包广播群
|
||||
if (message.groupID == SCGlobalConfig.bigBroadcastGroup ||
|
||||
message.groupID == roomRedPacketBroadcastGroupId) {
|
||||
_newBroadCastMsgRecv(message.groupID!, message);
|
||||
return;
|
||||
}
|
||||
|
||||
///群消息
|
||||
@ -560,6 +624,46 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return joinResult;
|
||||
}
|
||||
|
||||
Future<void> syncRoomRedPacketBroadcastGroup() async {
|
||||
for (var attempt = 0; attempt < 3 && !isLogout; attempt++) {
|
||||
try {
|
||||
final group = await SCChatRoomRepository().roomRedPacketImGroup();
|
||||
if (!group.canJoin) {
|
||||
return;
|
||||
}
|
||||
final newGroupId = group.groupId.trim();
|
||||
if (newGroupId == roomRedPacketBroadcastGroupId) {
|
||||
roomRedPacketBroadcastRegionCode = group.regionCode.trim();
|
||||
return;
|
||||
}
|
||||
final joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup(
|
||||
groupID: newGroupId,
|
||||
message: "",
|
||||
);
|
||||
if (joinResult.code == 0 || joinResult.code == 10013) {
|
||||
final oldGroupId = roomRedPacketBroadcastGroupId;
|
||||
roomRedPacketBroadcastGroupId = newGroupId;
|
||||
roomRedPacketBroadcastRegionCode = group.regionCode.trim();
|
||||
if (oldGroupId != null &&
|
||||
oldGroupId.isNotEmpty &&
|
||||
oldGroupId != newGroupId) {
|
||||
unawaited(quitGroup(oldGroupId));
|
||||
}
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] joined region group=$newGroupId region=${group.regionCode}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] join group failed code=${joinResult.code} desc=${joinResult.desc}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[RoomRedPacket][IM] sync group failed: $e');
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: 550));
|
||||
}
|
||||
}
|
||||
|
||||
///发送文本消息
|
||||
Future dispatchMessage(
|
||||
Msg msg, {
|
||||
@ -945,6 +1049,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.roomRedPacket) {
|
||||
roomChatMsgList.insert(0, msg);
|
||||
if (roomChatMsgList.length > 250) {
|
||||
print('大于200条消息');
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.gift ||
|
||||
msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
roomGiftMsgList.insert(0, msg);
|
||||
@ -959,6 +1070,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Msg _buildRoomRedPacketMsg(String groupId, dynamic data) {
|
||||
final payload =
|
||||
data is Map
|
||||
? data.map((key, value) => MapEntry(key.toString(), value))
|
||||
: <String, dynamic>{};
|
||||
final sender = SocialChatUserProfile(
|
||||
id: payload['actualAccount']?.toString() ?? payload['userId']?.toString(),
|
||||
userNickname:
|
||||
payload['userNickname']?.toString() ??
|
||||
payload['userName']?.toString(),
|
||||
userAvatar: payload['userAvatar']?.toString(),
|
||||
);
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: jsonEncode(payload),
|
||||
type: SCRoomMsgType.roomRedPacket,
|
||||
user: sender,
|
||||
);
|
||||
}
|
||||
|
||||
Msg? _mergeGiftMessageIfNeeded(Msg incoming) {
|
||||
if (incoming.type != SCRoomMsgType.gift &&
|
||||
incoming.type != SCRoomMsgType.luckGiftAnimOther) {
|
||||
@ -1192,6 +1323,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
bool isLogout = false;
|
||||
|
||||
logout() async {
|
||||
roomRedPacketBroadcastGroupId = null;
|
||||
roomRedPacketBroadcastRegionCode = null;
|
||||
V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout();
|
||||
TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
@ -1287,6 +1420,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
} else if (type == SCRoomMsgType.roomRedPacket) {
|
||||
///红包触发飘屏
|
||||
var fData = data["data"];
|
||||
if (!_isSameRoomRedPacketRegion(fData)) {
|
||||
return;
|
||||
}
|
||||
SCFloatingMessage msg = SCFloatingMessage(
|
||||
type: 4,
|
||||
roomId: fData["roomId"],
|
||||
@ -1294,6 +1430,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
userName: fData["userNickname"],
|
||||
userId: fData["actualAccount"],
|
||||
toUserId: fData["packetId"],
|
||||
coins: fData["totalAmount"],
|
||||
number: fData["totalCount"],
|
||||
priority: 1000,
|
||||
);
|
||||
if (msg.roomId ==
|
||||
@ -1301,10 +1439,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
context!,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).loadRoomRedPacketList(1);
|
||||
);
|
||||
rtcProvider.loadRoomRedPacketList(1);
|
||||
addMsg(
|
||||
_buildRoomRedPacketMsg(
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ??
|
||||
"",
|
||||
fData,
|
||||
),
|
||||
);
|
||||
}
|
||||
OverlayManager().addMessage(msg);
|
||||
} else if (type == SCRoomMsgType.inviteRoom) {
|
||||
@ -1351,6 +1497,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (data["type"] == SCRoomMsgType.roomRedPacket) {
|
||||
///房间红包
|
||||
var fData = data["data"];
|
||||
if (!_isSameRoomRedPacketRegion(fData)) {
|
||||
return;
|
||||
}
|
||||
SCFloatingMessage msg = SCFloatingMessage(
|
||||
type: 4,
|
||||
roomId: fData["roomId"],
|
||||
@ -1358,12 +1507,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
userName: fData["userNickname"],
|
||||
userId: fData["actualAccount"],
|
||||
toUserId: fData["packetId"],
|
||||
coins: fData["totalAmount"],
|
||||
number: fData["totalCount"],
|
||||
priority: 1000,
|
||||
);
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).loadRoomRedPacketList(1);
|
||||
addMsg(_buildRoomRedPacketMsg(groupID, fData));
|
||||
OverlayManager().addMessage(msg);
|
||||
return;
|
||||
}
|
||||
@ -1496,11 +1648,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_handleRoomLuckyGiftMessage(broadCastRes);
|
||||
} else {
|
||||
if (msg.type == SCRoomMsgType.joinRoom) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
);
|
||||
final shouldShowRoomVisualEffects =
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).shouldShowRoomVisualEffects;
|
||||
rtcProvider.shouldShowRoomVisualEffects;
|
||||
if (msg.user != null) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
@ -1512,14 +1665,38 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///坐骑
|
||||
if (msg.user?.getMountains() != null) {
|
||||
if (SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
final entryResource = msg.user?.getMountains();
|
||||
if (entryResource != null) {
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isEntryVehicleAnimation &&
|
||||
shouldShowRoomVisualEffects) {
|
||||
SCGiftVapSvgaManager().play(
|
||||
msg.user?.getMountains()?.sourceUrl ?? "",
|
||||
entryResource.sourceUrl ?? "",
|
||||
priority: SCGiftVapSvgaManager.entryEffectPriority,
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
final coverUrl = _resolveEntryCompensationCover(entryResource);
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage(
|
||||
type: 5,
|
||||
priority: 950,
|
||||
userId: msg.user?.id ?? "",
|
||||
roomId: roomId,
|
||||
userAvatarUrl: msg.user?.userAvatar ?? "",
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
giftUrl: coverUrl,
|
||||
fallbackUrl: coverUrl,
|
||||
),
|
||||
dedupKey: _entryFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
userId: msg.user?.id,
|
||||
resourceId: entryResource.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (msg.type == SCRoomMsgType.gift) {
|
||||
@ -1562,13 +1739,51 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
if (giftSourceUrl.isNotEmpty && special.isNotEmpty) {
|
||||
if (scGiftHasFullScreenEffect(special)) {
|
||||
if (SCGlobalConfig.isGiftSpecialEffects &&
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
rtcProvider.shouldShowRoomVisualEffects) {
|
||||
_giftFxLog(
|
||||
'trigger player play path=$giftSourceUrl '
|
||||
'giftId=${gift.id} giftName=${gift.giftName}',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(giftSourceUrl);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
rtcProvider.shouldShowRoomVisualEffects) {
|
||||
final roomId =
|
||||
(msg.msg?.trim().isNotEmpty ?? false)
|
||||
? msg.msg ?? ""
|
||||
: rtcProvider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.id ??
|
||||
"";
|
||||
final quantity = msg.number ?? 0;
|
||||
final coins = quantity * (gift.giftCandy ?? 0);
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
priority: 900,
|
||||
userId: msg.user?.id ?? "",
|
||||
toUserId: msg.toUser?.id ?? "",
|
||||
userAvatarUrl: msg.user?.userAvatar ?? "",
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
toUserName: msg.toUser?.userNickname ?? "",
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto,
|
||||
giftId: gift.id,
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
senderId: msg.user?.id,
|
||||
receiverId: msg.toUser?.id,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip player play because visual effects disabled '
|
||||
@ -1607,18 +1822,28 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0);
|
||||
if (coins > 9999) {
|
||||
OverlayManager().addMessage(
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
userId: msg.user?.id ?? "",
|
||||
toUserId: msg.toUser?.id ?? "",
|
||||
userAvatarUrl: msg.user?.userAvatar ?? "",
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
toUserName: msg.toUser?.userNickname ?? "",
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto,
|
||||
giftId: gift.id,
|
||||
number: msg.number,
|
||||
coins: coins,
|
||||
roomId: msg.msg,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: msg.msg,
|
||||
senderId: msg.user?.id,
|
||||
receiverId: msg.toUser?.id,
|
||||
giftId: gift.id,
|
||||
quantity: msg.number,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1739,7 +1964,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
groupID: SCGlobalConfig.bigBroadcastGroup,
|
||||
message: "",
|
||||
);
|
||||
if (joinResult.code == 0) {
|
||||
if (joinResult.code == 0 || joinResult.code == 10013) {
|
||||
joined = true;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -1782,6 +2007,22 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSameRoomRedPacketRegion(dynamic payload) {
|
||||
final map =
|
||||
payload is Map
|
||||
? payload.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
final packetRegion = map['regionCode']?.toString().trim();
|
||||
final currentRegion = roomRedPacketBroadcastRegionCode?.trim();
|
||||
if (packetRegion == null ||
|
||||
packetRegion.isEmpty ||
|
||||
currentRegion == null ||
|
||||
currentRegion.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return packetRegion.toUpperCase() == currentRegion.toUpperCase();
|
||||
}
|
||||
|
||||
///清屏
|
||||
void clearMessage() {
|
||||
roomAllMsgList.clear();
|
||||
|
||||
107
lib/shared/business_logic/models/res/sc_entry_popup_res.dart
Normal file
@ -0,0 +1,107 @@
|
||||
class SCEntryPopupRes {
|
||||
SCEntryPopupRes({
|
||||
required this.requestId,
|
||||
required this.queue,
|
||||
required this.popups,
|
||||
});
|
||||
|
||||
final String requestId;
|
||||
final List<String> queue;
|
||||
final Map<String, SCEntryPopupItem> popups;
|
||||
|
||||
factory SCEntryPopupRes.fromJson(dynamic json) {
|
||||
final map = json is Map<String, dynamic> ? json : const <String, dynamic>{};
|
||||
final queue =
|
||||
(map['queue'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.map((item) => item.toString())
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList();
|
||||
final popups = <String, SCEntryPopupItem>{};
|
||||
for (final entry in map.entries) {
|
||||
final key = entry.key;
|
||||
if (key == 'requestId' || key == 'queue') {
|
||||
continue;
|
||||
}
|
||||
final value = entry.value;
|
||||
if (value is Map<String, dynamic>) {
|
||||
popups[key] = SCEntryPopupItem.fromJson(key, value);
|
||||
}
|
||||
}
|
||||
return SCEntryPopupRes(
|
||||
requestId: map['requestId']?.toString() ?? '',
|
||||
queue: queue,
|
||||
popups: popups,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SCEntryPopupItem {
|
||||
SCEntryPopupItem({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.limit,
|
||||
required this.popupId,
|
||||
required this.version,
|
||||
required this.priority,
|
||||
required this.title,
|
||||
required this.image,
|
||||
required this.jumpType,
|
||||
required this.jumpUrl,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final bool value;
|
||||
final int limit;
|
||||
final String popupId;
|
||||
final int version;
|
||||
final int priority;
|
||||
final String title;
|
||||
final String image;
|
||||
final String jumpType;
|
||||
final String jumpUrl;
|
||||
final String reason;
|
||||
|
||||
factory SCEntryPopupItem.fromJson(String key, Map<String, dynamic> json) {
|
||||
return SCEntryPopupItem(
|
||||
key: key,
|
||||
value: _asBool(json['value']),
|
||||
limit: _asInt(json['limit']),
|
||||
popupId: json['popupId']?.toString() ?? '',
|
||||
version: _asInt(json['version']),
|
||||
priority: _asInt(json['priority']),
|
||||
title: json['title']?.toString() ?? '',
|
||||
image: json['image']?.toString() ?? '',
|
||||
jumpType: json['jumpType']?.toString() ?? '',
|
||||
jumpUrl: json['jumpUrl']?.toString() ?? '',
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _asBool(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return normalized == 'true' || normalized == '1';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
if (value is String) {
|
||||
return int.tryParse(value.trim()) ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
class SCRoomRedPacketConfigRes {
|
||||
const SCRoomRedPacketConfigRes({
|
||||
this.enabled = false,
|
||||
this.dailySendLimit = 0,
|
||||
this.todaySendCount = 0,
|
||||
this.delaySeconds = 0,
|
||||
this.refundAfterSeconds = 0,
|
||||
this.amountOptions = const <int>[],
|
||||
this.countOptions = const <int>[],
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketConfigRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketConfigRes(
|
||||
enabled: _boolValue(map['enabled']),
|
||||
dailySendLimit: _intValue(map['dailySendLimit']),
|
||||
todaySendCount: _intValue(map['todaySendCount']),
|
||||
delaySeconds: _intValue(map['delaySeconds']),
|
||||
refundAfterSeconds: _intValue(map['refundAfterSeconds']),
|
||||
amountOptions: _intListValue(map['amountOptions']),
|
||||
countOptions: _intListValue(map['countOptions']),
|
||||
);
|
||||
}
|
||||
|
||||
final bool enabled;
|
||||
final int dailySendLimit;
|
||||
final int todaySendCount;
|
||||
final int delaySeconds;
|
||||
final int refundAfterSeconds;
|
||||
final List<int> amountOptions;
|
||||
final List<int> countOptions;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'enabled': enabled,
|
||||
'dailySendLimit': dailySendLimit,
|
||||
'todaySendCount': todaySendCount,
|
||||
'delaySeconds': delaySeconds,
|
||||
'refundAfterSeconds': refundAfterSeconds,
|
||||
'amountOptions': amountOptions,
|
||||
'countOptions': countOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRedPacketImGroupRes {
|
||||
const SCRoomRedPacketImGroupRes({
|
||||
this.configured = false,
|
||||
this.sysOrigin = '',
|
||||
this.regionCode = '',
|
||||
this.groupId = '',
|
||||
this.groupName = '',
|
||||
this.status = '',
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketImGroupRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketImGroupRes(
|
||||
configured: _boolValue(map['configured']),
|
||||
sysOrigin: map['sysOrigin']?.toString() ?? '',
|
||||
regionCode: map['regionCode']?.toString() ?? '',
|
||||
groupId: map['groupId']?.toString() ?? '',
|
||||
groupName: map['groupName']?.toString() ?? '',
|
||||
status: map['status']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
final bool configured;
|
||||
final String sysOrigin;
|
||||
final String regionCode;
|
||||
final String groupId;
|
||||
final String groupName;
|
||||
final String status;
|
||||
|
||||
bool get canJoin => configured && groupId.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
class SCRoomRedPacketPresenceRes {
|
||||
const SCRoomRedPacketPresenceRes({
|
||||
this.userId = 0,
|
||||
this.sysOrigin = '',
|
||||
this.roomId = '',
|
||||
this.regionCode = '',
|
||||
this.ttlSeconds = 0,
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketPresenceRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketPresenceRes(
|
||||
userId: _intValue(map['userId']),
|
||||
sysOrigin: map['sysOrigin']?.toString() ?? '',
|
||||
roomId: map['roomId']?.toString() ?? '',
|
||||
regionCode: map['regionCode']?.toString() ?? '',
|
||||
ttlSeconds: _intValue(map['ttlSeconds']),
|
||||
);
|
||||
}
|
||||
|
||||
final int userId;
|
||||
final String sysOrigin;
|
||||
final String roomId;
|
||||
final String regionCode;
|
||||
final int ttlSeconds;
|
||||
}
|
||||
|
||||
int _intValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.round();
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value)?.round() ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _boolValue(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return normalized == 'true' || normalized == '1' || normalized == 'yes';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<int> _intListValue(dynamic value) {
|
||||
if (value is! List) {
|
||||
return const <int>[];
|
||||
}
|
||||
return value.map(_intValue).where((item) => item > 0).toList(growable: false);
|
||||
}
|
||||
@ -17,22 +17,23 @@
|
||||
|
||||
class SCRoomRedPacketDetailRes {
|
||||
SCRoomRedPacketDetailRes({
|
||||
String? packetId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? userAvatar,
|
||||
num? packetType,
|
||||
String? packetTypeDesc,
|
||||
num? totalAmount,
|
||||
num? totalCount,
|
||||
num? remainAmount,
|
||||
num? remainCount,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
num? myAmount,
|
||||
bool? grabbed,
|
||||
List<Records>? records,
|
||||
String? createTime,}){
|
||||
String? packetId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? userAvatar,
|
||||
num? packetType,
|
||||
String? packetTypeDesc,
|
||||
num? totalAmount,
|
||||
num? totalCount,
|
||||
num? remainAmount,
|
||||
num? remainCount,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
num? myAmount,
|
||||
bool? grabbed,
|
||||
List<Records>? records,
|
||||
String? createTime,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -49,23 +50,25 @@ class SCRoomRedPacketDetailRes {
|
||||
_grabbed = grabbed;
|
||||
_records = records;
|
||||
_createTime = createTime;
|
||||
}
|
||||
}
|
||||
|
||||
SCRoomRedPacketDetailRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_userId = json['userId'];
|
||||
_userName = json['userName'];
|
||||
_packetId = json['packetId']?.toString() ?? json['packetNo']?.toString();
|
||||
_userId = _numOrNull(json['userId']);
|
||||
_userName = json['userName'] ?? json['userNickname'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_packetType = json['packetType'];
|
||||
_packetType = _numOrNull(json['packetType']);
|
||||
_packetTypeDesc = json['packetTypeDesc'];
|
||||
_totalAmount = json['totalAmount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_remainAmount = json['remainAmount'];
|
||||
_remainCount = json['remainCount'];
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_myAmount = json['myAmount'];
|
||||
_totalAmount = _numOrNull(json['totalAmount']);
|
||||
_totalCount = _numOrNull(json['totalCount']);
|
||||
_remainAmount = _numOrNull(json['remainAmount']);
|
||||
_remainCount = _numOrNull(json['remainCount']);
|
||||
_status = _numOrNull(json['status']);
|
||||
_statusDesc = json['statusDesc']?.toString() ?? json['status']?.toString();
|
||||
_myAmount = _numOrNull(json['myAmount']);
|
||||
_grabbed = json['grabbed'];
|
||||
_expireTime = json['expireTime']?.toString();
|
||||
_claimStartTime = json['claimStartTime']?.toString();
|
||||
if (json['records'] != null) {
|
||||
_records = [];
|
||||
json['records'].forEach((v) {
|
||||
@ -90,6 +93,8 @@ class SCRoomRedPacketDetailRes {
|
||||
bool? _grabbed;
|
||||
List<Records>? _records;
|
||||
String? _createTime;
|
||||
String? _expireTime;
|
||||
String? _claimStartTime;
|
||||
|
||||
String? get packetId => _packetId;
|
||||
num? get userId => _userId;
|
||||
@ -107,6 +112,8 @@ class SCRoomRedPacketDetailRes {
|
||||
bool? get grabbed => _grabbed;
|
||||
List<Records>? get records => _records;
|
||||
String? get createTime => _createTime;
|
||||
String? get expireTime => _expireTime;
|
||||
String? get claimStartTime => _claimStartTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
@ -128,9 +135,10 @@ class SCRoomRedPacketDetailRes {
|
||||
map['records'] = _records?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
map['createTime'] = _createTime;
|
||||
map['expireTime'] = _expireTime;
|
||||
map['claimStartTime'] = _claimStartTime;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// recordId : ""
|
||||
@ -142,13 +150,14 @@ class SCRoomRedPacketDetailRes {
|
||||
|
||||
class Records {
|
||||
Records({
|
||||
String? recordId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? account,
|
||||
String? userAvatar,
|
||||
num? amount,
|
||||
String? grabTime,}){
|
||||
String? recordId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? account,
|
||||
String? userAvatar,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
}) {
|
||||
_recordId = recordId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -156,7 +165,7 @@ class Records {
|
||||
_userAvatar = userAvatar;
|
||||
_amount = amount;
|
||||
_grabTime = grabTime;
|
||||
}
|
||||
}
|
||||
|
||||
Records.fromJson(dynamic json) {
|
||||
_recordId = json['recordId'];
|
||||
@ -194,5 +203,14 @@ class Records {
|
||||
map['grabTime'] = _grabTime;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
num? _numOrNull(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -6,36 +6,42 @@
|
||||
|
||||
class SCRoomRedPacketGrabRes {
|
||||
SCRoomRedPacketGrabRes({
|
||||
String? packetId,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
num? remainCount,
|
||||
num? totalCount,}){
|
||||
String? packetId,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
num? remainCount,
|
||||
num? totalCount,
|
||||
String? status,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_amount = amount;
|
||||
_grabTime = grabTime;
|
||||
_remainCount = remainCount;
|
||||
_totalCount = totalCount;
|
||||
}
|
||||
_status = status;
|
||||
}
|
||||
|
||||
SCRoomRedPacketGrabRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_packetId = json['packetId'] ?? json['packetNo'];
|
||||
_amount = json['amount'];
|
||||
_grabTime = json['grabTime'];
|
||||
_grabTime = json['grabTime'] ?? json['claimTime'];
|
||||
_remainCount = json['remainCount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_status = json['status']?.toString();
|
||||
}
|
||||
String? _packetId;
|
||||
num? _amount;
|
||||
String? _grabTime;
|
||||
num? _remainCount;
|
||||
num? _totalCount;
|
||||
String? _status;
|
||||
|
||||
String? get packetId => _packetId;
|
||||
num? get amount => _amount;
|
||||
String? get grabTime => _grabTime;
|
||||
num? get remainCount => _remainCount;
|
||||
num? get totalCount => _totalCount;
|
||||
String? get status => _status;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
@ -44,7 +50,7 @@ class SCRoomRedPacketGrabRes {
|
||||
map['grabTime'] = _grabTime;
|
||||
map['remainCount'] = _remainCount;
|
||||
map['totalCount'] = _totalCount;
|
||||
map['status'] = _status;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
class SCRoomRedPacketListRes {
|
||||
SCRoomRedPacketListRes({
|
||||
String? packetId,
|
||||
String? packetMode,
|
||||
String? roomId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
@ -34,6 +35,8 @@ class SCRoomRedPacketListRes {
|
||||
num? remainCount,
|
||||
num? expireMinutes,
|
||||
String? expireTime,
|
||||
String? claimStartTime,
|
||||
num? balanceAfter,
|
||||
int? countdown,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
@ -41,6 +44,7 @@ class SCRoomRedPacketListRes {
|
||||
String? createTime,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_packetMode = packetMode;
|
||||
_roomId = roomId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -54,6 +58,8 @@ class SCRoomRedPacketListRes {
|
||||
_remainCount = remainCount;
|
||||
_expireMinutes = expireMinutes;
|
||||
_expireTime = expireTime;
|
||||
_claimStartTime = claimStartTime;
|
||||
_balanceAfter = balanceAfter;
|
||||
_countdown = countdown;
|
||||
_status = status;
|
||||
_statusDesc = statusDesc;
|
||||
@ -62,28 +68,33 @@ class SCRoomRedPacketListRes {
|
||||
}
|
||||
|
||||
SCRoomRedPacketListRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_roomId = json['roomId'];
|
||||
_userId = json['userId'];
|
||||
_userName = json['userName'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_packetType = json['packetType'];
|
||||
_packetTypeDesc = json['packetTypeDesc'];
|
||||
_sourceType = json['sourceType'];
|
||||
_totalAmount = json['totalAmount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_remainAmount = json['remainAmount'];
|
||||
_remainCount = json['remainCount'];
|
||||
_expireMinutes = json['expireMinutes'];
|
||||
_expireTime = json['expireTime'];
|
||||
_countdown = json['countdown'];
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_packetId = json['packetId']?.toString() ?? json['packetNo']?.toString();
|
||||
_packetMode = json['packetMode']?.toString();
|
||||
_roomId = json['roomId']?.toString();
|
||||
_userId = _numOrNull(json['userId']);
|
||||
_userName =
|
||||
json['userName']?.toString() ?? json['userNickname']?.toString();
|
||||
_userAvatar = json['userAvatar']?.toString();
|
||||
_packetType = _numOrNull(json['packetType']);
|
||||
_packetTypeDesc = json['packetTypeDesc']?.toString();
|
||||
_sourceType = _numOrNull(json['sourceType']);
|
||||
_totalAmount = _numOrNull(json['totalAmount']);
|
||||
_totalCount = _numOrNull(json['totalCount']);
|
||||
_remainAmount = _numOrNull(json['remainAmount']);
|
||||
_remainCount = _numOrNull(json['remainCount']);
|
||||
_expireMinutes = _numOrNull(json['expireMinutes']);
|
||||
_expireTime = json['expireTime']?.toString();
|
||||
_claimStartTime = json['claimStartTime']?.toString();
|
||||
_balanceAfter = _numOrNull(json['balanceAfter']);
|
||||
_countdown = _intOrNull(json['countdown']);
|
||||
_status = _numOrNull(json['status']);
|
||||
_statusDesc = json['statusDesc']?.toString() ?? json['status']?.toString();
|
||||
_grabbed = json['grabbed'];
|
||||
_createTime = json['createTime'];
|
||||
}
|
||||
|
||||
String? _packetId;
|
||||
String? _packetMode;
|
||||
String? _roomId;
|
||||
num? _userId;
|
||||
String? _userName;
|
||||
@ -97,6 +108,8 @@ class SCRoomRedPacketListRes {
|
||||
num? _remainCount;
|
||||
num? _expireMinutes;
|
||||
String? _expireTime;
|
||||
String? _claimStartTime;
|
||||
num? _balanceAfter;
|
||||
int? _countdown;
|
||||
num? _status;
|
||||
String? _statusDesc;
|
||||
@ -105,6 +118,8 @@ class SCRoomRedPacketListRes {
|
||||
|
||||
String? get packetId => _packetId;
|
||||
|
||||
String? get packetMode => _packetMode;
|
||||
|
||||
String? get roomId => _roomId;
|
||||
|
||||
num? get userId => _userId;
|
||||
@ -131,6 +146,10 @@ class SCRoomRedPacketListRes {
|
||||
|
||||
String? get expireTime => _expireTime;
|
||||
|
||||
String? get claimStartTime => _claimStartTime;
|
||||
|
||||
num? get balanceAfter => _balanceAfter;
|
||||
|
||||
int? get countdown => _countdown;
|
||||
|
||||
num? get status => _status;
|
||||
@ -144,6 +163,7 @@ class SCRoomRedPacketListRes {
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['packetId'] = _packetId;
|
||||
map['packetMode'] = _packetMode;
|
||||
map['roomId'] = _roomId;
|
||||
map['userId'] = _userId;
|
||||
map['userName'] = _userName;
|
||||
@ -157,6 +177,8 @@ class SCRoomRedPacketListRes {
|
||||
map['remainCount'] = _remainCount;
|
||||
map['expireMinutes'] = _expireMinutes;
|
||||
map['expireTime'] = _expireTime;
|
||||
map['claimStartTime'] = _claimStartTime;
|
||||
map['balanceAfter'] = _balanceAfter;
|
||||
map['countdown'] = _countdown;
|
||||
map['status'] = _status;
|
||||
map['statusDesc'] = _statusDesc;
|
||||
@ -165,3 +187,17 @@ class SCRoomRedPacketListRes {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
num? _numOrNull(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _intOrNull(dynamic value) {
|
||||
return _numOrNull(value)?.round();
|
||||
}
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
class SCRoomRedPacketSentRecordRes {
|
||||
const SCRoomRedPacketSentRecordRes({
|
||||
this.packetNo = '',
|
||||
this.roomId = '',
|
||||
this.packetMode = '',
|
||||
this.totalAmount = 0,
|
||||
this.totalCount = 0,
|
||||
this.remainAmount = 0,
|
||||
this.remainCount = 0,
|
||||
this.claimStartTime = '',
|
||||
this.expireTime = '',
|
||||
this.status = '',
|
||||
this.createTime = '',
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketSentRecordRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketSentRecordRes(
|
||||
packetNo: _stringValue(map, const ['packetNo', 'packetId', 'id']),
|
||||
roomId: _stringValue(map, const ['roomId']),
|
||||
packetMode: _stringValue(map, const ['packetMode']),
|
||||
totalAmount: _numValue(map, const ['totalAmount', 'amount']),
|
||||
totalCount: _numValue(map, const ['totalCount', 'count']),
|
||||
remainAmount: _numValue(map, const ['remainAmount']),
|
||||
remainCount: _numValue(map, const ['remainCount']),
|
||||
claimStartTime: _stringValue(map, const ['claimStartTime']),
|
||||
expireTime: _stringValue(map, const ['expireTime']),
|
||||
status: _stringValue(map, const ['status', 'statusDesc']),
|
||||
createTime: _stringValue(map, const ['createTime', 'sendTime', 'sentAt']),
|
||||
);
|
||||
}
|
||||
|
||||
final String packetNo;
|
||||
final String roomId;
|
||||
final String packetMode;
|
||||
final num totalAmount;
|
||||
final num totalCount;
|
||||
final num remainAmount;
|
||||
final num remainCount;
|
||||
final String claimStartTime;
|
||||
final String expireTime;
|
||||
final String status;
|
||||
final String createTime;
|
||||
}
|
||||
|
||||
String _stringValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final text = json[key]?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
num _numValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
final parsed = num.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
/// taskType : 0
|
||||
/// taskName : ""
|
||||
/// taskDesc : ""
|
||||
/// jumpPage : ""
|
||||
/// conditionType : ""
|
||||
/// sortOrder : 0
|
||||
/// taskId : 0
|
||||
/// taskStatus : 0
|
||||
/// cover : ""
|
||||
/// taskIcon : ""
|
||||
/// isRewardCollected : 0
|
||||
/// quantity : 0
|
||||
// taskType : 0
|
||||
// taskName : ""
|
||||
// taskDesc : ""
|
||||
// jumpPage : ""
|
||||
// conditionType : ""
|
||||
// sortOrder : 0
|
||||
// taskId : "0"
|
||||
// taskStatus : 0
|
||||
// cover : ""
|
||||
// taskIcon : ""
|
||||
// isRewardCollected : 0
|
||||
// quantity : 0
|
||||
|
||||
class SCTaskListRes {
|
||||
SCTaskListRes({
|
||||
@ -19,12 +19,12 @@ class SCTaskListRes {
|
||||
String? jumpPage,
|
||||
String? conditionType,
|
||||
num? sortOrder,
|
||||
num? taskId,
|
||||
String? taskId,
|
||||
num? taskStatus,
|
||||
String? cover,
|
||||
String? taskIcon,
|
||||
String? conditionValue,
|
||||
String? completedValue,
|
||||
num? completedValue,
|
||||
num? isRewardCollected,
|
||||
num? targetValue,
|
||||
num? quantity,
|
||||
@ -47,21 +47,21 @@ class SCTaskListRes {
|
||||
}
|
||||
|
||||
SCTaskListRes.fromJson(dynamic json) {
|
||||
_taskType = json['taskType'];
|
||||
_taskName = json['taskName'];
|
||||
_taskDesc = json['taskDesc'];
|
||||
_jumpPage = json['jumpPage'];
|
||||
_conditionType = json['conditionType'];
|
||||
_sortOrder = json['sortOrder'];
|
||||
_taskId = json['taskId'];
|
||||
_taskStatus = json['taskStatus'];
|
||||
_cover = json['cover'];
|
||||
_taskIcon = json['taskIcon'];
|
||||
_conditionValue = json['conditionValue'];
|
||||
_completedValue = json['completedValue'];
|
||||
_isRewardCollected = json['isRewardCollected'];
|
||||
_quantity = json['quantity'];
|
||||
_targetValue = json['targetValue'];
|
||||
_taskType = _asNum(json['taskType']);
|
||||
_taskName = _asString(json['taskName']);
|
||||
_taskDesc = _asString(json['taskDesc']);
|
||||
_jumpPage = _asString(json['jumpPage']);
|
||||
_conditionType = _asString(json['conditionType']);
|
||||
_sortOrder = _asNum(json['sortOrder']);
|
||||
_taskId = _asString(json['taskId']);
|
||||
_taskStatus = _asNum(json['taskStatus']);
|
||||
_cover = _asString(json['cover']);
|
||||
_taskIcon = _asString(json['taskIcon']);
|
||||
_conditionValue = _asString(json['conditionValue']);
|
||||
_completedValue = _asNum(json['completedValue']);
|
||||
_isRewardCollected = _asNum(json['isRewardCollected']);
|
||||
_quantity = _asNum(json['quantity']);
|
||||
_targetValue = _asNum(json['targetValue']);
|
||||
}
|
||||
|
||||
num? _taskType;
|
||||
@ -70,12 +70,12 @@ class SCTaskListRes {
|
||||
String? _jumpPage;
|
||||
String? _conditionType;
|
||||
num? _sortOrder;
|
||||
num? _taskId;
|
||||
String? _taskId;
|
||||
num? _taskStatus;
|
||||
String? _cover;
|
||||
String? _taskIcon;
|
||||
String? _conditionValue;
|
||||
String? _completedValue;
|
||||
num? _completedValue;
|
||||
num? _isRewardCollected;
|
||||
num? _quantity;
|
||||
num? _targetValue;
|
||||
@ -92,7 +92,7 @@ class SCTaskListRes {
|
||||
|
||||
num? get sortOrder => _sortOrder;
|
||||
|
||||
num? get taskId => _taskId;
|
||||
String? get taskId => _taskId;
|
||||
|
||||
num? get taskStatus => _taskStatus;
|
||||
|
||||
@ -102,7 +102,7 @@ class SCTaskListRes {
|
||||
|
||||
String? get conditionValue => _conditionValue;
|
||||
|
||||
String? get completedValue => _completedValue;
|
||||
num? get completedValue => _completedValue;
|
||||
|
||||
num? get isRewardCollected => _isRewardCollected;
|
||||
|
||||
@ -128,4 +128,15 @@ class SCTaskListRes {
|
||||
map['targetValue'] = _targetValue;
|
||||
return map;
|
||||
}
|
||||
|
||||
static String? _asString(dynamic value) {
|
||||
if (value == null) return null;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
static num? _asNum(dynamic value) {
|
||||
if (value is num) return value;
|
||||
if (value is String) return num.tryParse(value);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
|
||||
@ -39,6 +40,9 @@ abstract class SocialChatConfigRepository {
|
||||
///获取平台banner.
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>? types});
|
||||
|
||||
///APP进场弹窗.
|
||||
Future<SCEntryPopupRes> entryPopup();
|
||||
|
||||
///获取商品配置
|
||||
Future<List<SCProductConfigRes>> productConfig();
|
||||
|
||||
|
||||
@ -16,8 +16,10 @@ import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_join_black_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_record_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart';
|
||||
@ -217,8 +219,9 @@ abstract class SocialChatRoomRepository {
|
||||
String roomId,
|
||||
String totalAmount,
|
||||
String totalCount,
|
||||
String expireMinutes,
|
||||
);
|
||||
String expireMinutes, {
|
||||
String packetMode = 'DELAYED',
|
||||
});
|
||||
|
||||
///抢红包
|
||||
Future<SCRoomRedPacketGrabRes> roomRedPacketGrab(String packetId);
|
||||
@ -226,6 +229,26 @@ abstract class SocialChatRoomRepository {
|
||||
///查询红包详情
|
||||
Future<SCRoomRedPacketDetailRes> roomRedPacketDetail(String packetId);
|
||||
|
||||
///查询语音房红包配置
|
||||
Future<SCRoomRedPacketConfigRes> roomRedPacketConfig();
|
||||
|
||||
///查询当前用户语区红包 IM 广播群
|
||||
Future<SCRoomRedPacketImGroupRes> roomRedPacketImGroup();
|
||||
|
||||
///上报房间红包在线态
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceHeartbeat(
|
||||
String roomId,
|
||||
);
|
||||
|
||||
///离开房间红包在线态
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceLeave(String roomId);
|
||||
|
||||
///查询红包发送记录
|
||||
Future<List<SCRoomRedPacketSentRecordRes>> roomRedPacketSentRecords({
|
||||
int cursor = 1,
|
||||
int limit = 20,
|
||||
});
|
||||
|
||||
///查询房间奖励信息
|
||||
Future<SCRoomRewardInfoRes> roomRewardInfo(String roomId);
|
||||
|
||||
|
||||
@ -216,7 +216,10 @@ abstract class SocialChatUserRepository {
|
||||
Future<List<SCTaskListRes>> tasks();
|
||||
|
||||
///领取奖励
|
||||
Future<bool> taskReward(num taskId);
|
||||
Future<bool> taskReward(String taskId);
|
||||
|
||||
///可领取任务数量
|
||||
Future<int> taskClaimableCount();
|
||||
|
||||
///我的访客列表
|
||||
Future<List<FollowUserRes>> visitorList(num pageNumber);
|
||||
@ -242,6 +245,12 @@ abstract class SocialChatUserRepository {
|
||||
///赠送道具券
|
||||
Future<bool> couponSend(String couponNo, String receiverId);
|
||||
|
||||
///查询已拥有徽章
|
||||
Future<List<WearBadge>> badgeOwnList(String type);
|
||||
|
||||
///切换佩戴徽章
|
||||
Future<bool> badgeToggle(String badgeId);
|
||||
|
||||
///处理用户违规
|
||||
Future<SCViolationHandleRes> userViolationHandle(
|
||||
String userId,
|
||||
@ -283,4 +292,21 @@ abstract class SocialChatUserRepository {
|
||||
|
||||
///注销账号
|
||||
Future<bool> logoutAccount();
|
||||
|
||||
///申请 CP 关系
|
||||
Future<bool> cpRelationshipSendApply(String acceptApplyUserId);
|
||||
|
||||
///处理 CP 申请
|
||||
Future<bool> cpRelationshipProcessApply(String applyId, bool agree);
|
||||
|
||||
///解除 CP 申请/关系
|
||||
Future<bool> cpRelationshipDismissApply(String cpUserId);
|
||||
|
||||
///更新 VIP 能力配置
|
||||
Future<bool> userVipAbilityUpdate({
|
||||
String? contactMode,
|
||||
bool? kickPrevention,
|
||||
bool? mysteriousInvisibility,
|
||||
bool? antiBlock,
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,9 +21,13 @@ import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
typedef FloatingScreenManager = OverlayManager;
|
||||
|
||||
class OverlayManager {
|
||||
static const int _maxLowPerformanceCompensationQueueLength = 8;
|
||||
static const Duration _compensationDedupWindow = Duration(seconds: 3);
|
||||
|
||||
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
final Map<String, DateTime> _recentCompensationKeys = {};
|
||||
bool _isPlaying = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
@ -40,16 +44,9 @@ class OverlayManager {
|
||||
void addMessage(SCFloatingMessage message) {
|
||||
if (_isDisposed) return;
|
||||
if (SCGlobalConfig.isFloatingAnimationInGlobal) {
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
_enqueueMessage(
|
||||
message,
|
||||
debugLabel: 'floating_message_type_${message.type ?? -1}',
|
||||
action: () {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_messageQueue.clear();
|
||||
@ -59,6 +56,90 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
void addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (_isDisposed || !SCGlobalConfig.isLowPerformanceDevice) {
|
||||
return;
|
||||
}
|
||||
final key = dedupKey ?? _compensationKeyFor(message);
|
||||
if (_isRecentCompensationDuplicate(key)) {
|
||||
return;
|
||||
}
|
||||
_rememberCompensationKey(key);
|
||||
_trimLowPerformanceCompensationQueue();
|
||||
_enqueueMessage(
|
||||
message,
|
||||
debugLabel: 'low_performance_compensation_${message.type ?? -1}',
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueMessage(
|
||||
SCFloatingMessage message, {
|
||||
required String debugLabel,
|
||||
}) {
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
debugLabel: debugLabel,
|
||||
action: () {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecentCompensationDuplicate(String key) {
|
||||
_pruneRecentCompensationKeys();
|
||||
final recentTime = _recentCompensationKeys[key];
|
||||
if (recentTime == null) {
|
||||
return false;
|
||||
}
|
||||
return DateTime.now().difference(recentTime) < _compensationDedupWindow;
|
||||
}
|
||||
|
||||
void _rememberCompensationKey(String key) {
|
||||
_recentCompensationKeys[key] = DateTime.now();
|
||||
_pruneRecentCompensationKeys();
|
||||
}
|
||||
|
||||
void _pruneRecentCompensationKeys() {
|
||||
final now = DateTime.now();
|
||||
_recentCompensationKeys.removeWhere(
|
||||
(_, time) => now.difference(time) > _compensationDedupWindow,
|
||||
);
|
||||
}
|
||||
|
||||
String _compensationKeyFor(SCFloatingMessage message) {
|
||||
return [
|
||||
message.type,
|
||||
message.roomId,
|
||||
message.userId,
|
||||
message.toUserId,
|
||||
message.giftId,
|
||||
message.number,
|
||||
message.giftUrl,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
void _trimLowPerformanceCompensationQueue() {
|
||||
if (_messageQueue.length < _maxLowPerformanceCompensationQueueLength) {
|
||||
return;
|
||||
}
|
||||
final retained =
|
||||
_messageQueue.unorderedElements.cast<SCFloatingMessage>().toList()
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
_messageQueue.clear();
|
||||
for (final message in retained.take(
|
||||
_maxLowPerformanceCompensationQueueLength - 1,
|
||||
)) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warmMessageImages(SCFloatingMessage message) async {
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
@ -232,6 +313,7 @@ class OverlayManager {
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_messageQueue.clear();
|
||||
_recentCompensationKeys.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
}
|
||||
|
||||
@ -300,8 +300,13 @@ class BaseNetworkClient {
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return DioException(requestOptions: e.requestOptions, error: '接收超时');
|
||||
case DioExceptionType.badResponse:
|
||||
var errorCode = e.response?.data["errorCode"];
|
||||
var errorMsg = e.response?.data["errorMsg"];
|
||||
final responseData = e.response?.data;
|
||||
final errorCode =
|
||||
responseData is Map ? responseData["errorCode"] : null;
|
||||
final errorMsg =
|
||||
responseData is Map
|
||||
? responseData["errorMsg"]
|
||||
: responseData?.toString();
|
||||
if (errorCode == SCErroCode.userNotRegistered.code) {
|
||||
//用户还没有注册
|
||||
SCTts.show("Please register an account first.");
|
||||
@ -319,7 +324,7 @@ class BaseNetworkClient {
|
||||
!SCNavigatorUtils.inLoginPage &&
|
||||
!await _isCurrentSessionStillValid(e);
|
||||
final BuildContext? context = navigatorKey.currentContext;
|
||||
if (context != null && shouldLogout) {
|
||||
if (context != null && context.mounted && shouldLogout) {
|
||||
AccountStorage().logout(context);
|
||||
SCNavigatorUtils.inLoginPage = true;
|
||||
}
|
||||
@ -338,19 +343,22 @@ class BaseNetworkClient {
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
error: '服务器错误: ${e.response?.data["errorCode"]}',
|
||||
error: '服务器错误: $errorCode ${errorMsg ?? ""}'.trim(),
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
type: DioExceptionType.cancel,
|
||||
error: e.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,
|
||||
error: 'Net fail',
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
error: e.error ?? e.message ?? 'Net fail',
|
||||
message: e.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,26 +441,68 @@ class ResponseData<T> {
|
||||
}) {
|
||||
final dynamic responseBody =
|
||||
json.containsKey('body') ? json['body'] : json['data'];
|
||||
final success = _readSuccess(json);
|
||||
final errorCode = _readErrorCode(json);
|
||||
final errorCodeName =
|
||||
json['errorCodeName']?.toString() ?? json['code']?.toString();
|
||||
final errorMsg =
|
||||
json['errorMsg']?.toString() ?? json['message']?.toString();
|
||||
if (fromJsonT == null) {
|
||||
return ResponseData(
|
||||
status: json['status'],
|
||||
errorCode: json['errorCode'],
|
||||
errorCodeName: json['errorCodeName'],
|
||||
errorMsg: json['errorMsg'],
|
||||
status: success,
|
||||
errorCode: errorCode,
|
||||
errorCodeName: errorCodeName,
|
||||
errorMsg: errorMsg,
|
||||
body: responseBody,
|
||||
time: json['time'],
|
||||
);
|
||||
} else {
|
||||
return ResponseData(
|
||||
status: json['status'],
|
||||
errorCode: json['errorCode'],
|
||||
errorCodeName: json['errorCodeName'],
|
||||
errorMsg: json['errorMsg'],
|
||||
status: success,
|
||||
errorCode: errorCode,
|
||||
errorCodeName: errorCodeName,
|
||||
errorMsg: errorMsg,
|
||||
body: responseBody != null ? _parseData(responseBody, fromJsonT) : null,
|
||||
time: json['time'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _readSuccess(Map<String, dynamic> json) {
|
||||
final status = json['status'];
|
||||
if (status is bool) {
|
||||
return status;
|
||||
}
|
||||
if (status is num) {
|
||||
return status != 0;
|
||||
}
|
||||
if (status is String) {
|
||||
final normalized = status.trim().toLowerCase();
|
||||
if (normalized == 'true' || normalized == 'success') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == 'fail') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final code = json['code']?.toString().trim().toLowerCase();
|
||||
return code == 'success' || code == 'ok';
|
||||
}
|
||||
|
||||
static int? _readErrorCode(Map<String, dynamic> json) {
|
||||
final errorCode = json['errorCode'];
|
||||
if (errorCode is int) {
|
||||
return errorCode;
|
||||
}
|
||||
if (errorCode is num) {
|
||||
return errorCode.toInt();
|
||||
}
|
||||
if (errorCode is String) {
|
||||
return int.tryParse(errorCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 超时拦截器
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
@ -10,7 +14,10 @@ import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/config_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'
|
||||
show BaseNetworkClient;
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||
|
||||
class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
static SCConfigRepositoryImp? _instance;
|
||||
@ -48,6 +55,25 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///app/popups/entry
|
||||
@override
|
||||
Future<SCEntryPopupRes> entryPopup() async {
|
||||
final deviceId = await SCDeviceIdUtils.getDeviceId();
|
||||
final result = await http.post<SCEntryPopupRes>(
|
||||
"/app/popups/entry",
|
||||
data: {
|
||||
"scene": "APP_ENTER",
|
||||
"platform": Platform.isIOS ? "ios" : "android",
|
||||
"appVersion": SCGlobalConfig.version,
|
||||
"deviceId": deviceId,
|
||||
"language": SCGlobalConfig.lang,
|
||||
},
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCEntryPopupRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///sys/config/enum/config
|
||||
@override
|
||||
Future<CountryRes> getConfig() async {
|
||||
|
||||
@ -27,7 +27,9 @@ import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_record_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/room_repository.dart';
|
||||
@ -201,8 +203,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
num mickIndex, {
|
||||
String? eventType,
|
||||
String? inviterId,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
params["roomId"] = roomId;
|
||||
params["mickIndex"] = mickIndex;
|
||||
if (eventType != null) {
|
||||
@ -267,12 +269,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
silentErrorToast
|
||||
? const {BaseNetworkClient.silentErrorToastKey: true}
|
||||
: null,
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
|
||||
.toList(),
|
||||
);
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
|
||||
.toList(),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -321,8 +323,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||
String? dynamicContentId,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
if (roomId != null) {
|
||||
params["roomId"] = roomId;
|
||||
}
|
||||
@ -517,10 +519,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
bool? adminLockSeat,
|
||||
bool? showHeartbeat,
|
||||
bool? openKtvMode,
|
||||
String? roomSpecialMikeType,
|
||||
}) async {
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
Map<String, dynamic> params = {};
|
||||
String? roomSpecialMikeType,
|
||||
}) async {
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
Map<String, dynamic> params = {};
|
||||
params["roomId"] = roomId;
|
||||
if (touristMike != null) {
|
||||
params["touristMike"] = touristMike;
|
||||
@ -558,7 +560,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
fromJson: (json) => json as bool,
|
||||
);
|
||||
|
||||
rtmProvider.dispatchMessage(
|
||||
rtmProvider.dispatchMessage(
|
||||
Msg(
|
||||
groupId: roomAcount,
|
||||
msg: roomId,
|
||||
@ -796,16 +798,20 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String roomId,
|
||||
String totalAmount,
|
||||
String totalCount,
|
||||
String expireMinutes,
|
||||
) async {
|
||||
String expireMinutes, {
|
||||
String packetMode = 'DELAYED',
|
||||
}) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["requestId"] =
|
||||
"room-red-packet-${DateTime.now().microsecondsSinceEpoch}";
|
||||
parm["roomId"] = roomId;
|
||||
parm["packetMode"] = packetMode;
|
||||
parm["totalAmount"] = totalAmount;
|
||||
parm["totalCount"] = totalCount;
|
||||
parm["expireMinutes"] = expireMinutes;
|
||||
final result = await http.post<SCRoomRedPacketListRes>(
|
||||
"318de84592dca60c287ba2f54d60756af01ca103e12cc332b59b5a8e39c778ec",
|
||||
"/app/voice-room/red-packet/send",
|
||||
data: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketListRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
@ -815,10 +821,11 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
@override
|
||||
Future<SCRoomRedPacketGrabRes> roomRedPacketGrab(String packetId) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["packetId"] = packetId;
|
||||
parm["packetNo"] = packetId;
|
||||
final result = await http.post<SCRoomRedPacketGrabRes>(
|
||||
"318de84592dca60c287ba2f54d60756a0b2a61d0ce4f3f4d50875dcc95b640af",
|
||||
"/app/voice-room/red-packet/claim",
|
||||
data: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketGrabRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
@ -828,15 +835,90 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
@override
|
||||
Future<SCRoomRedPacketDetailRes> roomRedPacketDetail(String packetId) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["packetId"] = packetId;
|
||||
parm["packetNo"] = packetId;
|
||||
final result = await http.get<SCRoomRedPacketDetailRes>(
|
||||
"318de84592dca60c287ba2f54d60756a7f342deecb1b83d2d8429c59146f10f0",
|
||||
"/app/voice-room/red-packet/detail",
|
||||
queryParams: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketDetailRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketConfigRes> roomRedPacketConfig() async {
|
||||
return http.get<SCRoomRedPacketConfigRes>(
|
||||
"/app/voice-room/red-packet/config",
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketConfigRes.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketImGroupRes> roomRedPacketImGroup() async {
|
||||
return http.get<SCRoomRedPacketImGroupRes>(
|
||||
"/app/voice-room/red-packet/im-group",
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketImGroupRes.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceHeartbeat(
|
||||
String roomId,
|
||||
) async {
|
||||
return http.post<SCRoomRedPacketPresenceRes>(
|
||||
"/app/voice-room/red-packet/presence/heartbeat",
|
||||
data: {"roomId": roomId},
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketPresenceRes.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceLeave(
|
||||
String roomId,
|
||||
) async {
|
||||
return http.post<SCRoomRedPacketPresenceRes>(
|
||||
"/app/voice-room/red-packet/presence/leave",
|
||||
data: {"roomId": roomId},
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketPresenceRes.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SCRoomRedPacketSentRecordRes>> roomRedPacketSentRecords({
|
||||
int cursor = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
return http.get<List<SCRoomRedPacketSentRecordRes>>(
|
||||
"/app/voice-room/red-packet/sent-records",
|
||||
queryParams: {"cursor": cursor, "limit": limit},
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson:
|
||||
(json) =>
|
||||
_redPacketRecordListBody(
|
||||
json,
|
||||
).map((e) => SCRoomRedPacketSentRecordRes.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<dynamic> _redPacketRecordListBody(dynamic json) {
|
||||
if (json is List) {
|
||||
return json;
|
||||
}
|
||||
if (json is Map) {
|
||||
for (final key in const ['records', 'list', 'items']) {
|
||||
final value = json[key];
|
||||
if (value is List) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return const <dynamic>[];
|
||||
}
|
||||
|
||||
///activity/room-contribution-activity/room-reward/info
|
||||
@override
|
||||
Future<SCRoomRewardInfoRes> roomRewardInfo(String roomId) async {
|
||||
|
||||
@ -32,6 +32,8 @@ import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/user_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'
|
||||
show BaseNetworkClient;
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
import 'package:yumi/shared/tools/sc_mobile_login_context.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_profile_cache.dart';
|
||||
@ -962,29 +964,161 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///app/v2/task/list
|
||||
///app/task-center/list
|
||||
@override
|
||||
Future<List<SCTaskListRes>> tasks() async {
|
||||
try {
|
||||
final tasks = await _taskList("/app/task-center/list", source: "new");
|
||||
if (kDebugMode && tasks.isEmpty) {
|
||||
await _debugProbeLegacyTaskList();
|
||||
}
|
||||
return tasks;
|
||||
} catch (error) {
|
||||
_debugTaskError("new list", error);
|
||||
return _taskList(
|
||||
"6e87048da643aa0388a9e46ea391daff574aff257ce7e668d08f4caccd1c6232",
|
||||
source: "legacy",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SCTaskListRes>> _taskList(
|
||||
String path, {
|
||||
required String source,
|
||||
}) async {
|
||||
debugPrint("[TaskCenter][Repo] list request source=$source path=$path");
|
||||
final result = await http.get<List<SCTaskListRes>>(
|
||||
"6e87048da643aa0388a9e46ea391daff574aff257ce7e668d08f4caccd1c6232",
|
||||
path,
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List).map((e) => SCTaskListRes.fromJson(e)).toList(),
|
||||
);
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] list response source=$source "
|
||||
"count=${result.length} ${_taskListDebugSample(result)}",
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///app/v2/task/reward
|
||||
Future<void> _debugProbeLegacyTaskList() async {
|
||||
try {
|
||||
await _taskList(
|
||||
"6e87048da643aa0388a9e46ea391daff574aff257ce7e668d08f4caccd1c6232",
|
||||
source: "legacy-debug-probe",
|
||||
);
|
||||
} catch (error) {
|
||||
_debugTaskError("legacy debug probe", error);
|
||||
}
|
||||
}
|
||||
|
||||
String _taskListDebugSample(List<SCTaskListRes> tasks) {
|
||||
if (tasks.isEmpty) return "sample=empty";
|
||||
final sample = tasks
|
||||
.take(3)
|
||||
.map((task) {
|
||||
return "{id=${task.taskId},type=${task.taskType},"
|
||||
"status=${task.taskStatus},collected=${task.isRewardCollected},"
|
||||
"current=${task.completedValue},target=${task.targetValue},"
|
||||
"quantity=${task.quantity},name=${task.taskName}}";
|
||||
})
|
||||
.join(" ");
|
||||
return "sample=$sample";
|
||||
}
|
||||
|
||||
///app/task-center/claim
|
||||
@override
|
||||
Future<bool> taskReward(num taskId) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["taskId"] = taskId;
|
||||
final result = await http.get<bool>(
|
||||
"0047abf8502867ed03180e3a6b505d411832ea9bebe4ec426a7036c865082475",
|
||||
queryParams: parm,
|
||||
fromJson: (json) => json as bool,
|
||||
Future<bool> taskReward(String taskId) async {
|
||||
try {
|
||||
debugPrint("[TaskCenter][Repo] claim request source=new taskId=$taskId");
|
||||
final result = await http.post<bool>(
|
||||
"/app/task-center/claim",
|
||||
data: {"taskId": taskId},
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => json as bool,
|
||||
);
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] claim response source=new taskId=$taskId "
|
||||
"success=$result",
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
_debugTaskError("new claim taskId=$taskId", error);
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] claim request source=legacy taskId=$taskId",
|
||||
);
|
||||
final result = await http.get<bool>(
|
||||
"0047abf8502867ed03180e3a6b505d411832ea9bebe4ec426a7036c865082475",
|
||||
queryParams: {"taskId": taskId},
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => json as bool,
|
||||
);
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] claim response source=legacy taskId=$taskId "
|
||||
"success=$result",
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
///app/task-center/claimable-count
|
||||
@override
|
||||
Future<int> taskClaimableCount() async {
|
||||
try {
|
||||
return await _taskClaimableCount("/app/task-center/claimable-count");
|
||||
} catch (error) {
|
||||
_debugTaskError("new count", error);
|
||||
return _taskClaimableCount("/app/v2/task/claimable-count");
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _taskClaimableCount(String path) async {
|
||||
debugPrint("[TaskCenter][Repo] claimable count request path=$path");
|
||||
final result = await http.get<num>(
|
||||
path,
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: _parseTaskClaimableCount,
|
||||
);
|
||||
return result;
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] claimable count response path=$path count=$result",
|
||||
);
|
||||
return result.toInt();
|
||||
}
|
||||
|
||||
num _parseTaskClaimableCount(dynamic json) {
|
||||
final value = json is Map ? json["count"] : json;
|
||||
if (value is num) return value;
|
||||
if (value is String) return num.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _debugTaskError(String stage, Object error) {
|
||||
if (error is DioException) {
|
||||
debugPrint(
|
||||
"[TaskCenter][Repo] $stage failed "
|
||||
"type=${error.type} status=${error.response?.statusCode} "
|
||||
"message=${error.message} error=${error.error} "
|
||||
"url=${error.requestOptions.uri} "
|
||||
"data=${_clipTaskDebug(_stringifyTaskDebug(error.response?.data))}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
debugPrint("[TaskCenter][Repo] $stage failed error=$error");
|
||||
}
|
||||
|
||||
String _stringifyTaskDebug(dynamic data) {
|
||||
if (data == null) return "null";
|
||||
try {
|
||||
return jsonEncode(data);
|
||||
} catch (_) {
|
||||
return data.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String _clipTaskDebug(String text) {
|
||||
const maxLength = 1200;
|
||||
if (text.length <= maxLength) return text;
|
||||
return "${text.substring(0, maxLength)}...";
|
||||
}
|
||||
|
||||
///user/support-relation/visitor/list
|
||||
|
||||
@ -215,6 +215,7 @@ class SCVapTask implements Comparable<SCVapTask> {
|
||||
return hashCode.compareTo(other.hashCode);
|
||||
}
|
||||
}
|
||||
|
||||
// 优先队列实现
|
||||
class SCPriorityQueue<E> {
|
||||
final List<E> _els = [];
|
||||
@ -242,5 +243,5 @@ class SCPriorityQueue<E> {
|
||||
|
||||
void clear() => _els.clear();
|
||||
|
||||
List<E> get unorderedElements => List.from(_els);
|
||||
}
|
||||
List<E> get unorderedElements => List.from(_els);
|
||||
}
|
||||
|
||||
385
lib/shared/tools/sc_entry_popup_coordinator.dart
Normal file
@ -0,0 +1,385 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/chat/chat_route.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/task/task_route.dart';
|
||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_h5_url_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
import 'package:yumi/ui_kit/widgets/entry_popup/entry_popup_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart';
|
||||
|
||||
typedef SCEntryPopupRootTabSelector = void Function(int index);
|
||||
|
||||
class SCEntryPopupCoordinator {
|
||||
static const Duration _requestMinGap = Duration(seconds: 2);
|
||||
static const Duration _roomReadyTimeout = Duration(seconds: 16);
|
||||
|
||||
static bool _isRunning = false;
|
||||
static DateTime? _lastRequestAt;
|
||||
|
||||
static Future<void> showIfNeeded(
|
||||
BuildContext context, {
|
||||
SCEntryPopupRootTabSelector? onSelectRootTab,
|
||||
}) async {
|
||||
if (_isRunning || !context.mounted || AccountStorage().getToken().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final lastRequestAt = _lastRequestAt;
|
||||
if (lastRequestAt != null &&
|
||||
now.difference(lastRequestAt) < _requestMinGap) {
|
||||
return;
|
||||
}
|
||||
_isRunning = true;
|
||||
_lastRequestAt = now;
|
||||
try {
|
||||
final response = await SCConfigRepositoryImp().entryPopup();
|
||||
final popup = _pickDisplayablePopup(response);
|
||||
if (popup == null) {
|
||||
return;
|
||||
}
|
||||
final imageReady = await warmImageResource(
|
||||
popup.image,
|
||||
logicalWidth: 326,
|
||||
timeout: const Duration(seconds: 8),
|
||||
);
|
||||
if (!imageReady || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await SCEntryPopupDialog.show(
|
||||
context,
|
||||
popup: popup,
|
||||
onDisplayed: () {
|
||||
unawaited(_markShown(popup));
|
||||
},
|
||||
onTap: () {
|
||||
unawaited(_handlePopupTap(context, popup, onSelectRootTab));
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('[EntryPopup] show failed: $error');
|
||||
} finally {
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
static SCEntryPopupItem? _pickDisplayablePopup(SCEntryPopupRes response) {
|
||||
for (final popupKey in response.queue) {
|
||||
final popup = response.popups[popupKey];
|
||||
if (popup == null || !_canShow(popup)) {
|
||||
continue;
|
||||
}
|
||||
return popup;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _canShow(SCEntryPopupItem popup) {
|
||||
if (!popup.value || popup.image.trim().isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (popup.limit <= 0) {
|
||||
return true;
|
||||
}
|
||||
final lastShownAt = DataPersistence.getInt(_shownStorageKey(popup));
|
||||
if (lastShownAt <= 0) {
|
||||
return true;
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
return nowMs - lastShownAt >= Duration(days: popup.limit).inMilliseconds;
|
||||
}
|
||||
|
||||
static Future<bool> _markShown(SCEntryPopupItem popup) {
|
||||
return DataPersistence.setInt(
|
||||
_shownStorageKey(popup),
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
static String _shownStorageKey(SCEntryPopupItem popup) {
|
||||
final profile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final userId = _firstNonEmpty([profile?.id, profile?.account, 'guest']);
|
||||
return 'app_popup_last_show:$userId:${popup.key}:${popup.version}';
|
||||
}
|
||||
|
||||
static Future<void> _handlePopupTap(
|
||||
BuildContext context,
|
||||
SCEntryPopupItem popup,
|
||||
SCEntryPopupRootTabSelector? onSelectRootTab,
|
||||
) async {
|
||||
SmartDialog.dismiss(tag: SCEntryPopupDialog.dialogTag);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 120));
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final jumpType = popup.jumpType.trim().toUpperCase();
|
||||
if (jumpType.isEmpty || jumpType == 'NOOP') {
|
||||
return;
|
||||
}
|
||||
if (jumpType == 'H5') {
|
||||
await _openH5(context, popup.jumpUrl);
|
||||
return;
|
||||
}
|
||||
if (jumpType == 'GAME') {
|
||||
await _openGameCenter(context, popup.jumpUrl);
|
||||
return;
|
||||
}
|
||||
if (jumpType == 'APP_ROUTE') {
|
||||
await _openAppRoute(context, popup.jumpUrl, onSelectRootTab);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _openH5(BuildContext context, String rawUrl) async {
|
||||
final url = _resolveH5Url(rawUrl);
|
||||
if (url.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (SCUrlLauncherUtils.shouldOpenExternally(url)) {
|
||||
final launched = await SCUrlLauncherUtils.launchExternal(url);
|
||||
if (launched || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final h5Url = SCH5UrlUtils.appendAppParams(url);
|
||||
await SCNavigatorUtils.push(
|
||||
context,
|
||||
'${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(h5Url)}&showTitle=false',
|
||||
);
|
||||
}
|
||||
|
||||
static String _resolveH5Url(String rawUrl) {
|
||||
final trimmedUrl = rawUrl.trim();
|
||||
if (trimmedUrl.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final uri = Uri.tryParse(trimmedUrl);
|
||||
if (uri == null || uri.hasScheme) {
|
||||
return trimmedUrl;
|
||||
}
|
||||
final baseUri = Uri.tryParse(SCGlobalConfig.userAgreementUrl);
|
||||
if (baseUri == null || baseUri.host.isEmpty) {
|
||||
return trimmedUrl;
|
||||
}
|
||||
return Uri(
|
||||
scheme: baseUri.scheme,
|
||||
host: baseUri.host,
|
||||
path: uri.path.startsWith('/') ? uri.path : '/${uri.path}',
|
||||
query: uri.query.isEmpty ? null : uri.query,
|
||||
).toString();
|
||||
}
|
||||
|
||||
static Future<void> _openAppRoute(
|
||||
BuildContext context,
|
||||
String rawUrl,
|
||||
SCEntryPopupRootTabSelector? onSelectRootTab,
|
||||
) async {
|
||||
final uri = Uri.tryParse(rawUrl.trim());
|
||||
final path = uri?.path ?? rawUrl.trim();
|
||||
final params = uri?.queryParameters ?? const <String, String>{};
|
||||
|
||||
switch (path) {
|
||||
case '/tab/home':
|
||||
onSelectRootTab?.call(0);
|
||||
return;
|
||||
case '/tab/explore':
|
||||
onSelectRootTab?.call(1);
|
||||
return;
|
||||
case '/tab/message':
|
||||
onSelectRootTab?.call(2);
|
||||
return;
|
||||
case '/tab/me':
|
||||
onSelectRootTab?.call(3);
|
||||
return;
|
||||
case '/search':
|
||||
await SCNavigatorUtils.push(context, SCMainRoute.mainSearch);
|
||||
return;
|
||||
case '/task':
|
||||
await SCNavigatorUtils.push(context, TaskRoute.task);
|
||||
return;
|
||||
case '/store':
|
||||
await SCNavigatorUtils.push(context, StoreRoute.list);
|
||||
return;
|
||||
case '/bag':
|
||||
await SCNavigatorUtils.push(context, StoreRoute.bags);
|
||||
return;
|
||||
case '/recharge':
|
||||
await SCNavigatorUtils.push(context, WalletRoute.recharge);
|
||||
return;
|
||||
case '/gold-record':
|
||||
await SCNavigatorUtils.push(context, WalletRoute.goldRecord);
|
||||
return;
|
||||
case '/vip':
|
||||
await SCNavigatorUtils.push(context, VipRoute.detail);
|
||||
return;
|
||||
case '/vip/benefit':
|
||||
await SCNavigatorUtils.push(
|
||||
context,
|
||||
'${VipRoute.benefit}?index=${Uri.encodeComponent(params['index'] ?? '0')}',
|
||||
);
|
||||
return;
|
||||
case '/profile':
|
||||
await _openProfile(context, params['userId']);
|
||||
return;
|
||||
case '/room':
|
||||
await _openRoom(context, params['roomId']);
|
||||
return;
|
||||
case '/game-center':
|
||||
await _openGameCenter(context, rawUrl);
|
||||
return;
|
||||
case '/message/activity':
|
||||
await SCNavigatorUtils.push(context, SCChatRouter.activity);
|
||||
return;
|
||||
case '/message/notification':
|
||||
await SCNavigatorUtils.push(context, SCChatRouter.notifcation);
|
||||
return;
|
||||
case '/language':
|
||||
await SCNavigatorUtils.push(context, SCMainRoute.language);
|
||||
return;
|
||||
case '/settings':
|
||||
await SCNavigatorUtils.push(context, SettingsRoute.settings);
|
||||
return;
|
||||
case '/me/follow':
|
||||
await SCNavigatorUtils.push(context, SCMainRoute.follow);
|
||||
return;
|
||||
case '/me/fans':
|
||||
await SCNavigatorUtils.push(context, SCMainRoute.fans);
|
||||
return;
|
||||
case '/me/visitors':
|
||||
await SCNavigatorUtils.push(context, SCMainRoute.vistors);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _openProfile(BuildContext context, String? userId) async {
|
||||
final resolvedUserId = (userId ?? '').trim();
|
||||
if (resolvedUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? '';
|
||||
await SCNavigatorUtils.push(
|
||||
context,
|
||||
'${SCMainRoute.person}?isMe=${currentUserId == resolvedUserId}&tageId=${Uri.encodeComponent(resolvedUserId)}',
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _openRoom(BuildContext context, String? roomId) async {
|
||||
final resolvedRoomId = (roomId ?? '').trim();
|
||||
if (resolvedRoomId.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await _enterRoom(context, resolvedRoomId);
|
||||
}
|
||||
|
||||
static Future<void> _openGameCenter(
|
||||
BuildContext context,
|
||||
String rawUrl,
|
||||
) async {
|
||||
final uri = Uri.tryParse(rawUrl.trim());
|
||||
final params = uri?.queryParameters ?? const <String, String>{};
|
||||
final roomId = (params['roomId'] ?? '').trim();
|
||||
final gameId = (params['gameId'] ?? '').trim();
|
||||
if (roomId.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
final ready = await _enterRoom(context, roomId);
|
||||
if (!ready || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomGameBottomSheet(
|
||||
roomContext: context,
|
||||
initialGameId: gameId.isEmpty ? null : gameId,
|
||||
),
|
||||
barrierColor: Colors.black54,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> _enterRoom(BuildContext context, String roomId) async {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
RoomEntryPreviewData? previewData;
|
||||
try {
|
||||
final room = await SCChatRoomRepository().specific(roomId);
|
||||
previewData = RoomEntryPreviewData.fromMyRoom(room);
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'[EntryPopup] load room preview failed roomId=$roomId: $error',
|
||||
);
|
||||
}
|
||||
if (!context.mounted) {
|
||||
return false;
|
||||
}
|
||||
rtcProvider.joinVoiceRoomSession(context, roomId, previewData: previewData);
|
||||
return _waitForRoomReady(rtcProvider, roomId);
|
||||
}
|
||||
|
||||
static Future<bool> _waitForRoomReady(
|
||||
RtcProvider rtcProvider,
|
||||
String roomId,
|
||||
) {
|
||||
if (_isRoomReady(rtcProvider, roomId)) {
|
||||
return Future<bool>.value(true);
|
||||
}
|
||||
|
||||
final completer = Completer<bool>();
|
||||
Timer? timer;
|
||||
late VoidCallback listener;
|
||||
void complete(bool value) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
timer?.cancel();
|
||||
rtcProvider.removeListener(listener);
|
||||
completer.complete(value);
|
||||
}
|
||||
|
||||
listener = () {
|
||||
if (_isRoomReady(rtcProvider, roomId)) {
|
||||
complete(true);
|
||||
return;
|
||||
}
|
||||
if (rtcProvider.roomStartupStatus == RoomStartupStatus.failed) {
|
||||
complete(false);
|
||||
}
|
||||
};
|
||||
|
||||
rtcProvider.addListener(listener);
|
||||
timer = Timer(_roomReadyTimeout, () => complete(false));
|
||||
listener();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static bool _isRoomReady(RtcProvider rtcProvider, String roomId) {
|
||||
return rtcProvider.roomStartupStatus == RoomStartupStatus.ready &&
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id == roomId;
|
||||
}
|
||||
|
||||
static String _firstNonEmpty(List<String?> values) {
|
||||
for (final value in values) {
|
||||
final normalized = value?.trim() ?? '';
|
||||
if (normalized.isNotEmpty) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -46,20 +46,31 @@ Widget head({
|
||||
double? height,
|
||||
Border? border,
|
||||
String? headdress,
|
||||
String? headdressCover,
|
||||
BoxShape shape = BoxShape.circle,
|
||||
BoxFit fit = BoxFit.cover,
|
||||
BorderRadius? borderRadius,
|
||||
bool showDefault = true,
|
||||
bool isRoom = false,
|
||||
}) {
|
||||
final String headdressUrl = headdress?.trim() ?? "";
|
||||
final String headdressCoverUrl = headdressCover?.trim() ?? "";
|
||||
final String headdressExtension =
|
||||
SCPathUtils.getFileExtension(headdressUrl).toLowerCase();
|
||||
final bool hasPictureHeaddress =
|
||||
headdress != null &&
|
||||
headdress.isNotEmpty &&
|
||||
SCPathUtils.fileTypeIsPic2(headdress);
|
||||
headdressUrl.isNotEmpty && SCPathUtils.fileTypeIsPic2(headdressUrl);
|
||||
final bool hasAnimatedHeaddress =
|
||||
headdressExtension == ".svga" || headdressExtension == ".mp4";
|
||||
final bool shouldShowAnimatedHeaddress =
|
||||
hasAnimatedHeaddress && SCGlobalConfig.allowsHighCostAnimations;
|
||||
final bool shouldShowStaticHeaddressFallback =
|
||||
hasAnimatedHeaddress &&
|
||||
!SCGlobalConfig.allowsHighCostAnimations &&
|
||||
headdressCoverUrl.isNotEmpty;
|
||||
final bool hasHeaddress =
|
||||
headdressUrl.isNotEmpty || shouldShowStaticHeaddressFallback;
|
||||
final double avatarScale =
|
||||
hasPictureHeaddress
|
||||
? 0.64
|
||||
: (headdress != null && headdress.isNotEmpty ? 0.72 : 1);
|
||||
hasPictureHeaddress ? 0.64 : (hasHeaddress ? 0.72 : 1);
|
||||
final double avatarWidth = width * avatarScale;
|
||||
final double avatarHeight = (height ?? width) * avatarScale;
|
||||
|
||||
@ -138,7 +149,7 @@ Widget head({
|
||||
0,
|
||||
]),
|
||||
child: netImage(
|
||||
url: headdress,
|
||||
url: headdressUrl,
|
||||
width: width,
|
||||
height: height ?? width,
|
||||
fit: BoxFit.contain,
|
||||
@ -146,22 +157,31 @@ Widget head({
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
headdress != null &&
|
||||
SCPathUtils.getFileExtension(headdress).toLowerCase() ==
|
||||
".svga" &&
|
||||
SCGlobalConfig.allowsHighCostAnimations
|
||||
shouldShowStaticHeaddressFallback
|
||||
? IgnorePointer(
|
||||
child: netImage(
|
||||
url: headdressCoverUrl,
|
||||
width: width,
|
||||
height: height ?? width,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
headdressUrl.isNotEmpty &&
|
||||
headdressExtension == ".svga" &&
|
||||
shouldShowAnimatedHeaddress
|
||||
? IgnorePointer(
|
||||
child: SVGAHeadwearWidget(
|
||||
width: width,
|
||||
height: width,
|
||||
resource: headdress,
|
||||
resource: headdressUrl,
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
headdress != null &&
|
||||
SCPathUtils.getFileExtension(headdress).toLowerCase() ==
|
||||
".mp4" &&
|
||||
SCGlobalConfig.allowsHighCostAnimations
|
||||
headdressUrl.isNotEmpty &&
|
||||
headdressExtension == ".mp4" &&
|
||||
shouldShowAnimatedHeaddress
|
||||
? SizedBox(
|
||||
width: width,
|
||||
height: width,
|
||||
@ -170,11 +190,11 @@ Widget head({
|
||||
scaleType: ScaleType.centerCrop,
|
||||
repeat: 90000000,
|
||||
onViewCreated: (controller) {
|
||||
FileCacheManager.getInstance().getFile(url: headdress).then(
|
||||
(file) {
|
||||
controller.playFile(file.path);
|
||||
},
|
||||
);
|
||||
FileCacheManager.getInstance()
|
||||
.getFile(url: headdressUrl)
|
||||
.then((file) {
|
||||
controller.playFile(file.path);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -72,15 +72,16 @@ class _PropsBagHeaddressDetailDialogState
|
||||
),
|
||||
SizedBox(height: 25.w),
|
||||
head(
|
||||
url:
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.userAvatar ??
|
||||
"",
|
||||
width: 95.w,
|
||||
height: 95.w,
|
||||
headdress: widget.res.propsResources?.sourceUrl
|
||||
url:
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.userAvatar ??
|
||||
"",
|
||||
width: 95.w,
|
||||
height: 95.w,
|
||||
headdress: widget.res.propsResources?.sourceUrl,
|
||||
headdressCover: widget.res.propsResources?.cover,
|
||||
),
|
||||
SizedBox(height: 30.w),
|
||||
GestureDetector(
|
||||
@ -159,8 +160,11 @@ class _PropsBagHeaddressDetailDialogState
|
||||
unload,
|
||||
)
|
||||
.then((value) async {
|
||||
await Provider.of<SocialChatUserProfileManager>(context, listen: false).fetchUserProfileData(loadGuardCount: false);
|
||||
Future.delayed(Duration(milliseconds: 400),(){
|
||||
await Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData(loadGuardCount: false);
|
||||
Future.delayed(Duration(milliseconds: 400), () {
|
||||
SCLoadingManager.hide();
|
||||
SmartDialog.dismiss(tag: "showPropsDetail");
|
||||
});
|
||||
@ -169,8 +173,9 @@ class _PropsBagHeaddressDetailDialogState
|
||||
} else {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded);
|
||||
}
|
||||
}).catchError((e){
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
})
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
116
lib/ui_kit/widgets/entry_popup/entry_popup_dialog.dart
Normal file
@ -0,0 +1,116 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
|
||||
class SCEntryPopupDialog extends StatefulWidget {
|
||||
const SCEntryPopupDialog({
|
||||
super.key,
|
||||
required this.popup,
|
||||
required this.onTap,
|
||||
this.onDisplayed,
|
||||
});
|
||||
|
||||
static const String dialogTag = 'showEntryPopupDialog';
|
||||
|
||||
final SCEntryPopupItem popup;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onDisplayed;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required SCEntryPopupItem popup,
|
||||
required VoidCallback onTap,
|
||||
VoidCallback? onDisplayed,
|
||||
}) async {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
await SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.center,
|
||||
maskColor: Colors.black.withValues(alpha: 0.58),
|
||||
animationType: SmartAnimationType.fade,
|
||||
clickMaskDismiss: true,
|
||||
builder:
|
||||
(_) => SCEntryPopupDialog(
|
||||
popup: popup,
|
||||
onTap: onTap,
|
||||
onDisplayed: onDisplayed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<SCEntryPopupDialog> createState() => _SCEntryPopupDialogState();
|
||||
}
|
||||
|
||||
class _SCEntryPopupDialogState extends State<SCEntryPopupDialog> {
|
||||
bool _hasNotifiedDisplayed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _hasNotifiedDisplayed) {
|
||||
return;
|
||||
}
|
||||
_hasNotifiedDisplayed = true;
|
||||
widget.onDisplayed?.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final dialogWidth = math.min(screenWidth - 42.w, 326.w);
|
||||
final imageUrl = widget.popup.image.trim();
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: dialogWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onTap,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
child: Image(
|
||||
image: buildCachedImageProvider(
|
||||
imageUrl,
|
||||
logicalWidth: dialogWidth,
|
||||
),
|
||||
width: dialogWidth,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap:
|
||||
() =>
|
||||
SmartDialog.dismiss(tag: SCEntryPopupDialog.dialogTag),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.w),
|
||||
child: Image.asset(
|
||||
'sc_images/general/sc_icon_pic_close.png',
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
@ -64,6 +65,10 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
}
|
||||
|
||||
void _enqueueVipEntryFloatingMessage(Msg msg) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
((msg.user?.getMountains()?.sourceUrl ?? "").trim().isNotEmpty)) {
|
||||
return;
|
||||
}
|
||||
final floatPicture = msg.user?.getFloatPicture();
|
||||
final floatPictureUrl = _resolveVipFloatPictureUrl(floatPicture);
|
||||
final floatPictureFallbackUrl = _resolveVipFloatPictureFallbackUrl(
|
||||
@ -81,23 +86,39 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
if (roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final isLowPerformance = SCGlobalConfig.isLowPerformanceDevice;
|
||||
final displayUrl =
|
||||
isLowPerformance && floatPictureFallbackUrl.isNotEmpty
|
||||
? floatPictureFallbackUrl
|
||||
: floatPictureUrl;
|
||||
debugPrint(
|
||||
'[VIP][RoomFloat] enqueue entry float '
|
||||
'userId=${msg.user?.id} roomId=$roomId resourceId=${floatPicture?.id} '
|
||||
'url=$floatPictureUrl',
|
||||
'url=$displayUrl',
|
||||
);
|
||||
OverlayManager().addMessage(
|
||||
SCFloatingMessage(
|
||||
type: 5,
|
||||
userId: msg.user?.id,
|
||||
roomId: roomId,
|
||||
userAvatarUrl: msg.user?.userAvatar,
|
||||
userName: msg.user?.userNickname,
|
||||
giftUrl: floatPictureUrl,
|
||||
fallbackUrl: floatPictureFallbackUrl,
|
||||
priority: 900,
|
||||
),
|
||||
final floatingMessage = SCFloatingMessage(
|
||||
type: 5,
|
||||
userId: msg.user?.id,
|
||||
roomId: roomId,
|
||||
userAvatarUrl: msg.user?.userAvatar,
|
||||
userName: msg.user?.userNickname,
|
||||
giftUrl: displayUrl,
|
||||
fallbackUrl: floatPictureFallbackUrl,
|
||||
priority: 900,
|
||||
);
|
||||
if (isLowPerformance) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
floatingMessage,
|
||||
dedupKey: [
|
||||
"entry_float",
|
||||
roomId,
|
||||
msg.user?.id ?? "",
|
||||
floatPicture?.id ?? "",
|
||||
].join("|"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(floatingMessage);
|
||||
}
|
||||
|
||||
String _resolveVipFloatPictureUrl(PropsResources? resource) {
|
||||
|
||||
@ -1,228 +1,376 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||
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/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
|
||||
class FloatingRoomRedenvelopeScreenWidget extends StatefulWidget {
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted; // 动画完成回调
|
||||
|
||||
const FloatingRoomRedenvelopeScreenWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FloatingRoomRedenvelopeScreenWidgetState createState() =>
|
||||
_FloatingRoomRedenvelopeScreenWidgetState();
|
||||
}
|
||||
|
||||
class _FloatingRoomRedenvelopeScreenWidgetState
|
||||
extends State<FloatingRoomRedenvelopeScreenWidget>
|
||||
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();
|
||||
|
||||
// 停顿2秒(你代码中是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: _buildRedenvelopeAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
// 否则使用原来的动画
|
||||
return SlideTransition(
|
||||
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
|
||||
child: _buildRedenvelopeAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRedenvelopeAnimation() {
|
||||
// 使用Positioned.fill确保飘屏覆盖全屏宽度,并定位在特定高度(如顶部100像素)
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
if (widget.message.roomId != null &&
|
||||
widget.message.roomId!.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(
|
||||
widget.message.roomId!,
|
||||
navigatorKey.currentState!.context,
|
||||
needOpenRedenvelope: true,
|
||||
redPackId: widget.message.toUserId ?? "",
|
||||
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: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
height: 120.w,
|
||||
width: 325.w,
|
||||
margin: EdgeInsets.only(top: 10.w),
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 5.w,
|
||||
top: 31.w,
|
||||
child: head(
|
||||
url: widget.message.userAvatarUrl ?? "",
|
||||
width: 63.w,
|
||||
),
|
||||
),
|
||||
Image.asset("sc_images/room/sc_icon_redenvelope_broad_bg.webp"),
|
||||
PositionedDirectional(
|
||||
start: 70.w,
|
||||
top: 52.w,
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.openTheTreasureChest,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理向左滑动
|
||||
void _handleSwipeLeft() {
|
||||
if (_isSwipeAnimating) return;
|
||||
|
||||
setState(() {
|
||||
_isSwipeAnimating = true;
|
||||
});
|
||||
|
||||
// 停止主动画
|
||||
_controller.stop();
|
||||
|
||||
// 启动滑动动画
|
||||
_swipeController.reset();
|
||||
_swipeController.forward();
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
|
||||
|
||||
class FloatingRoomRedenvelopeScreenWidget extends StatefulWidget {
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted; // 动画完成回调
|
||||
|
||||
const FloatingRoomRedenvelopeScreenWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingRoomRedenvelopeScreenWidget> createState() =>
|
||||
_FloatingRoomRedenvelopeScreenWidgetState();
|
||||
}
|
||||
|
||||
class _FloatingRoomRedenvelopeScreenWidgetState
|
||||
extends State<FloatingRoomRedenvelopeScreenWidget>
|
||||
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();
|
||||
|
||||
// 停顿2秒(你代码中是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: _buildRedenvelopeAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
// 否则使用原来的动画
|
||||
return SlideTransition(
|
||||
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
|
||||
child: _buildRedenvelopeAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRedenvelopeAnimation() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: _handleTap,
|
||||
);
|
||||
},
|
||||
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: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 66.w,
|
||||
width: 326.w,
|
||||
margin: EdgeInsets.only(top: 42.w),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
// 设计稿飘窗是一个低矮红条,左右素材都压在红条上;
|
||||
// 下列数值只负责红条本身,头像/红包/GO 分别在独立常量里调。
|
||||
start: 24.w,
|
||||
end: 34.w,
|
||||
top: 13.w,
|
||||
child: Container(
|
||||
height: 43.w,
|
||||
padding: EdgeInsetsDirectional.fromSTEB(50.w, 6.w, 80.w, 5.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFF3F3F), Color(0xFFE91F25)],
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD484).withValues(alpha: 0.88),
|
||||
width: 0.8.w,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 8.w,
|
||||
offset: Offset(0, 2.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SizedBox(
|
||||
width: 138.w,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_senderNameForFloating(),
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
height: 1.08,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
Text(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.roomRedPacketFloatingSubtitle,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.86),
|
||||
fontSize: 10.sp,
|
||||
height: 1.05,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: 14.w,
|
||||
top: 13.w,
|
||||
child: _buildAvatar(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 66.w,
|
||||
top: -1.w,
|
||||
child: Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 54.w,
|
||||
height: 59.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 0,
|
||||
top: 7.w,
|
||||
child: Image.asset(
|
||||
'sc_images/room/red_packet/floating_go.png',
|
||||
width: 54.w,
|
||||
height: 54.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleTap() async {
|
||||
final roomId = widget.message.roomId ?? "";
|
||||
if (roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final currentContext = navigatorKey.currentState?.context ?? context;
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
currentContext,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id;
|
||||
if (currentRoomId == roomId) {
|
||||
final fallbackData = RoomRedPacketUiData.fromJson({
|
||||
'packetId': widget.message.toUserId,
|
||||
'userName': widget.message.userName,
|
||||
'userAvatar': widget.message.userAvatarUrl,
|
||||
'userId': widget.message.userId,
|
||||
'totalAmount': widget.message.coins,
|
||||
'totalCount': widget.message.number,
|
||||
'remainCount': widget.message.number,
|
||||
});
|
||||
var dialogData = fallbackData;
|
||||
try {
|
||||
final detail = await SCChatRoomRepository().roomRedPacketDetail(
|
||||
fallbackData.packetId,
|
||||
);
|
||||
dialogData = RoomRedPacketUiData.fromJson(detail.toJson());
|
||||
} catch (_) {}
|
||||
if (mounted) {
|
||||
final showContext = navigatorKey.currentState?.context;
|
||||
if (showContext != null && showContext.mounted) {
|
||||
RoomRedPacketOpenDialog.show(showContext, dialogData);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
SCRoomUtils.goRoom(
|
||||
roomId,
|
||||
currentContext,
|
||||
needOpenRedenvelope: true,
|
||||
redPackId: widget.message.toUserId ?? "",
|
||||
fromFloting: true,
|
||||
);
|
||||
}
|
||||
|
||||
String _senderNameForFloating() {
|
||||
final name = widget.message.userName?.trim() ?? "";
|
||||
if (name.isNotEmpty) {
|
||||
return name;
|
||||
}
|
||||
return SCAppLocalizations.of(context)!.redEnvelope;
|
||||
}
|
||||
|
||||
Widget _buildAvatar() {
|
||||
final avatarUrl = widget.message.userAvatarUrl?.trim() ?? "";
|
||||
return Container(
|
||||
width: 41.w,
|
||||
height: 41.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2.w),
|
||||
),
|
||||
child: ClipOval(
|
||||
child:
|
||||
avatarUrl.isNotEmpty
|
||||
? netImage(
|
||||
url: avatarUrl,
|
||||
width: 41.w,
|
||||
height: 41.w,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
color: const Color(0xFFFFEFE4),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 24.w,
|
||||
height: 26.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理向左滑动
|
||||
void _handleSwipeLeft() {
|
||||
if (_isSwipeAnimating) return;
|
||||
|
||||
setState(() {
|
||||
_isSwipeAnimating = true;
|
||||
});
|
||||
|
||||
// 停止主动画
|
||||
_controller.stop();
|
||||
|
||||
// 启动滑动动画
|
||||
_swipeController.reset();
|
||||
_swipeController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ class _FloatingVipEntryScreenWidgetState
|
||||
with TickerProviderStateMixin {
|
||||
static const double _bannerWidth = 350;
|
||||
static const double _bannerHeight = 84;
|
||||
static const String _fallbackEntryBgAsset =
|
||||
"sc_images/vip/sc_vip_entry_bg.png";
|
||||
static const String _avatarDynamicKey = "avatar";
|
||||
static const String _nicknameDynamicKey = "nickname";
|
||||
|
||||
@ -203,19 +205,13 @@ class _FloatingVipEntryScreenWidgetState
|
||||
}
|
||||
|
||||
Widget _buildFallbackBackground() {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_bannerHeight.w / 2),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6573FF), Color(0xFF8D5CFF)],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 10.w,
|
||||
offset: Offset(0, 4.w),
|
||||
),
|
||||
],
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_bannerHeight.w / 2),
|
||||
child: Image.asset(
|
||||
_fallbackEntryBgAsset,
|
||||
width: _bannerWidth.w,
|
||||
height: _bannerHeight.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
|
||||
|
||||
class RoomRedPacketChatMessage extends StatelessWidget {
|
||||
const RoomRedPacketChatMessage({super.key, required this.data});
|
||||
|
||||
final RoomRedPacketUiData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SCDebounceWidget(
|
||||
debounceTime: const Duration(milliseconds: 220),
|
||||
onTap: () => RoomRedPacketOpenDialog.show(context, data),
|
||||
child: Container(
|
||||
width: 300.w,
|
||||
constraints: BoxConstraints(minHeight: 78.w),
|
||||
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
|
||||
padding: EdgeInsetsDirectional.fromSTEB(16.w, 10.w, 10.w, 10.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFF4B43), Color(0xFFFF2E2F)],
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 10.w,
|
||||
offset: Offset(0, 3.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations.roomRedPacketChatMessage,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
height: 1.28,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 48.w,
|
||||
height: 54.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
|
||||
String roomRedPacketErrorCode(Object error) {
|
||||
if (error is DioException) {
|
||||
final data = error.response?.data;
|
||||
if (data is Map) {
|
||||
for (final key in const ['code', 'errorCodeName', 'errorCode']) {
|
||||
final code = data[key]?.toString().trim() ?? '';
|
||||
if (code.isNotEmpty) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String roomRedPacketErrorMessage(BuildContext context, Object error) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
switch (roomRedPacketErrorCode(error)) {
|
||||
case 'voice_room_region_not_match':
|
||||
return localizations.roomRedPacketRegionNotMatch;
|
||||
case 'voice_room_red_packet_not_started':
|
||||
return localizations.roomRedPacketNotStarted;
|
||||
case 'voice_room_red_packet_expired':
|
||||
return localizations.roomRedPacketOpenExpired;
|
||||
case 'voice_room_red_packet_finished':
|
||||
return localizations.roomRedPacketOpenSoldOut;
|
||||
case 'voice_room_red_packet_daily_limit_exceeded':
|
||||
return localizations.roomRedPacketDailyLimitExceeded;
|
||||
case 'voice_room_red_packet_insufficient_balance':
|
||||
return localizations.roomRedPacketInsufficientBalance;
|
||||
case 'voice_room_red_packet_room_presence_required':
|
||||
return localizations.roomRedPacketPresenceRequired;
|
||||
}
|
||||
|
||||
if (error is DioException) {
|
||||
final data = error.response?.data;
|
||||
if (data is Map) {
|
||||
final message =
|
||||
data['message']?.toString().trim() ??
|
||||
data['errorMsg']?.toString().trim() ??
|
||||
'';
|
||||
if (message.isNotEmpty && message.toLowerCase() != 'success') {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
return localizations.roomRedPacketNetworkError;
|
||||
}
|
||||
@ -0,0 +1,319 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_record_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
|
||||
class RoomRedPacketHistoryPage extends StatefulWidget {
|
||||
const RoomRedPacketHistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<RoomRedPacketHistoryPage> createState() =>
|
||||
_RoomRedPacketHistoryPageState();
|
||||
}
|
||||
|
||||
class _RoomRedPacketHistoryPageState extends State<RoomRedPacketHistoryPage> {
|
||||
static const Color _backgroundColor = Color(0xFF052821);
|
||||
bool _loading = true;
|
||||
List<_RoomRedPacketHistoryItem> _items = const <_RoomRedPacketHistoryItem>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRecords();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_RoomRedPacketHistoryTopBar(
|
||||
title: localizations.roomRedPacketHistoryTitle,
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
_loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFFF4A3C),
|
||||
),
|
||||
)
|
||||
: _items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
localizations.noData,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
20.w,
|
||||
10.w,
|
||||
20.w,
|
||||
24.w,
|
||||
),
|
||||
itemCount: _items.length,
|
||||
separatorBuilder:
|
||||
(_, __) => Divider(
|
||||
height: 10.w,
|
||||
thickness: 0.5.w,
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _RoomRedPacketHistoryTile(item: _items[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadRecords() async {
|
||||
try {
|
||||
final records = await SCChatRoomRepository().roomRedPacketSentRecords();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_items = records
|
||||
.map(_RoomRedPacketHistoryItem.fromRecord)
|
||||
.toList(growable: false);
|
||||
_loading = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketHistoryTile extends StatelessWidget {
|
||||
const _RoomRedPacketHistoryTile({required this.item});
|
||||
|
||||
final _RoomRedPacketHistoryItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
final isRtl = Directionality.of(context) == TextDirection.rtl;
|
||||
return SizedBox(
|
||||
height: 48.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/room/red_packet/small_packet_icon.png',
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
isRtl ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
localizations.roomRedPacketHistoryItemTitle(
|
||||
item.count.toString(),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Text(
|
||||
item.sentAt,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.45),
|
||||
fontSize: 9.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
isRtl ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatSignedAmount(item.amount),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Text(
|
||||
_statusText(localizations, item.status),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: _statusColor(item.status),
|
||||
fontSize: 8.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusText(
|
||||
SCAppLocalizations localizations,
|
||||
_RoomRedPacketHistoryStatus status,
|
||||
) {
|
||||
switch (status) {
|
||||
case _RoomRedPacketHistoryStatus.sent:
|
||||
return localizations.roomRedPacketHistoryStatusSent;
|
||||
case _RoomRedPacketHistoryStatus.pending:
|
||||
return localizations.roomRedPacketHistoryStatusPending;
|
||||
case _RoomRedPacketHistoryStatus.returned:
|
||||
return localizations.roomRedPacketHistoryStatusReturned;
|
||||
}
|
||||
}
|
||||
|
||||
Color _statusColor(_RoomRedPacketHistoryStatus status) {
|
||||
switch (status) {
|
||||
case _RoomRedPacketHistoryStatus.sent:
|
||||
return const Color(0xFFFF4A3C);
|
||||
case _RoomRedPacketHistoryStatus.pending:
|
||||
return const Color(0xFFFFC857);
|
||||
case _RoomRedPacketHistoryStatus.returned:
|
||||
return const Color(0xFF66D7A3);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatSignedAmount(int value) {
|
||||
final sign = value < 0 ? '-' : '+';
|
||||
final raw = value.abs().toString();
|
||||
final buffer = StringBuffer(sign);
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
if (i > 0 && (raw.length - i) % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(raw[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketHistoryTopBar extends StatelessWidget {
|
||||
const _RoomRedPacketHistoryTopBar({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textDirection = Directionality.of(context);
|
||||
return SizedBox(
|
||||
height: 52.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 6.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: Icon(
|
||||
textDirection == TextDirection.rtl
|
||||
? Icons.chevron_right
|
||||
: Icons.chevron_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 58.w),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketHistoryItem {
|
||||
const _RoomRedPacketHistoryItem({
|
||||
required this.count,
|
||||
required this.amount,
|
||||
required this.sentAt,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final int count;
|
||||
final int amount;
|
||||
final String sentAt;
|
||||
final _RoomRedPacketHistoryStatus status;
|
||||
|
||||
factory _RoomRedPacketHistoryItem.fromRecord(
|
||||
SCRoomRedPacketSentRecordRes record,
|
||||
) {
|
||||
return _RoomRedPacketHistoryItem(
|
||||
count: record.totalCount.round(),
|
||||
amount: -record.totalAmount.round(),
|
||||
sentAt: _formatTime(record.createTime),
|
||||
status: _statusFromRecord(record),
|
||||
);
|
||||
}
|
||||
|
||||
static _RoomRedPacketHistoryStatus _statusFromRecord(
|
||||
SCRoomRedPacketSentRecordRes record,
|
||||
) {
|
||||
final status = record.status.toLowerCase();
|
||||
if (status.contains('return') || status.contains('refund')) {
|
||||
return _RoomRedPacketHistoryStatus.returned;
|
||||
}
|
||||
if (status.contains('pending') || status.contains('active')) {
|
||||
return _RoomRedPacketHistoryStatus.pending;
|
||||
}
|
||||
return _RoomRedPacketHistoryStatus.sent;
|
||||
}
|
||||
|
||||
static String _formatTime(String raw) {
|
||||
final parsed = DateTime.tryParse(raw);
|
||||
if (parsed == null) {
|
||||
return raw;
|
||||
}
|
||||
String two(int value) => value.toString().padLeft(2, '0');
|
||||
return '${parsed.year}.${two(parsed.month)}.${two(parsed.day)} '
|
||||
'${two(parsed.hour)}:${two(parsed.minute)}:${two(parsed.second)}';
|
||||
}
|
||||
}
|
||||
|
||||
enum _RoomRedPacketHistoryStatus { sent, pending, returned }
|
||||
304
lib/ui_kit/widgets/room/red_packet/room_red_packet_models.dart
Normal file
@ -0,0 +1,304 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
|
||||
class RoomRedPacketUiData {
|
||||
const RoomRedPacketUiData({
|
||||
required this.packetId,
|
||||
required this.senderName,
|
||||
required this.senderAvatar,
|
||||
required this.senderId,
|
||||
required this.totalAmount,
|
||||
required this.totalCount,
|
||||
required this.remainCount,
|
||||
required this.expireMinutes,
|
||||
required this.countdown,
|
||||
required this.status,
|
||||
required this.statusDesc,
|
||||
required this.grabbed,
|
||||
required this.claimedAmount,
|
||||
required this.createTime,
|
||||
required this.claimStartTimeMs,
|
||||
required this.expireTimeMs,
|
||||
});
|
||||
|
||||
factory RoomRedPacketUiData.mock() {
|
||||
return const RoomRedPacketUiData(
|
||||
packetId: 'mock-room-red-packet',
|
||||
senderName: 'Jack',
|
||||
senderAvatar: '',
|
||||
senderId: '',
|
||||
totalAmount: 100000,
|
||||
totalCount: 5,
|
||||
remainCount: 5,
|
||||
expireMinutes: 5,
|
||||
countdown: 0,
|
||||
status: 1,
|
||||
statusDesc: '',
|
||||
grabbed: false,
|
||||
claimedAmount: 0,
|
||||
createTime: '',
|
||||
claimStartTimeMs: 0,
|
||||
expireTimeMs: 0,
|
||||
);
|
||||
}
|
||||
|
||||
factory RoomRedPacketUiData.fromListRes(SCRoomRedPacketListRes res) {
|
||||
return RoomRedPacketUiData(
|
||||
packetId: res.packetId ?? '',
|
||||
senderName: res.userName ?? '',
|
||||
senderAvatar: res.userAvatar ?? '',
|
||||
senderId: res.userId?.toString() ?? '',
|
||||
totalAmount: res.totalAmount ?? 0,
|
||||
totalCount: res.totalCount ?? 0,
|
||||
remainCount: res.remainCount ?? res.totalCount ?? 0,
|
||||
expireMinutes: res.expireMinutes ?? 0,
|
||||
countdown: res.countdown ?? 0,
|
||||
status: res.status ?? 0,
|
||||
statusDesc: res.statusDesc ?? '',
|
||||
grabbed: res.grabbed ?? false,
|
||||
claimedAmount: 0,
|
||||
createTime: res.createTime ?? '',
|
||||
claimStartTimeMs: _timestampValueFromDynamic(res.claimStartTime),
|
||||
expireTimeMs: _timestampValueFromDynamic(res.expireTime),
|
||||
);
|
||||
}
|
||||
|
||||
factory RoomRedPacketUiData.fromMessagePayload({
|
||||
required String? payload,
|
||||
SocialChatUserProfile? fallbackUser,
|
||||
}) {
|
||||
final decoded = _decodePayload(payload);
|
||||
return RoomRedPacketUiData.fromJson(decoded, fallbackUser: fallbackUser);
|
||||
}
|
||||
|
||||
factory RoomRedPacketUiData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
SocialChatUserProfile? fallbackUser,
|
||||
}) {
|
||||
final fallbackName = fallbackUser?.userNickname ?? '';
|
||||
final fallbackAvatar = fallbackUser?.userAvatar ?? '';
|
||||
final fallbackId = fallbackUser?.id ?? '';
|
||||
return RoomRedPacketUiData(
|
||||
packetId: _stringValue(json, const ['packetId', 'packetNo', 'id']),
|
||||
senderName: _stringValue(json, const [
|
||||
'userName',
|
||||
'userNickname',
|
||||
'nickname',
|
||||
'senderName',
|
||||
], fallback: fallbackName),
|
||||
senderAvatar: _stringValue(json, const [
|
||||
'userAvatar',
|
||||
'avatar',
|
||||
'senderAvatar',
|
||||
], fallback: fallbackAvatar),
|
||||
senderId: _stringValue(json, const [
|
||||
'userId',
|
||||
'actualAccount',
|
||||
'senderId',
|
||||
], fallback: fallbackId),
|
||||
totalAmount: _numValue(json, const ['totalAmount']),
|
||||
totalCount: _numValue(json, const ['totalCount', 'count']),
|
||||
remainCount: _numValue(json, const ['remainCount', 'leftCount']),
|
||||
expireMinutes: _numValue(json, const ['expireMinutes']),
|
||||
countdown: _intValue(json, const ['countdown']),
|
||||
status: _numValue(json, const ['status']),
|
||||
statusDesc: _stringValue(json, const ['statusDesc', 'status']),
|
||||
grabbed:
|
||||
_boolValue(json, const ['grabbed', 'claimed']) ||
|
||||
_numValue(json, const ['myAmount']) > 0,
|
||||
claimedAmount: _numValue(json, const ['myAmount', 'claimedAmount']),
|
||||
createTime: _stringValue(json, const ['createTime', 'sentAt']),
|
||||
claimStartTimeMs: _timestampValue(json, const ['claimStartTime']),
|
||||
expireTimeMs: _timestampValue(json, const ['expireTime']),
|
||||
);
|
||||
}
|
||||
|
||||
final String packetId;
|
||||
final String senderName;
|
||||
final String senderAvatar;
|
||||
final String senderId;
|
||||
final num totalAmount;
|
||||
final num totalCount;
|
||||
final num remainCount;
|
||||
final num expireMinutes;
|
||||
final int countdown;
|
||||
final num status;
|
||||
final String statusDesc;
|
||||
final bool grabbed;
|
||||
final num claimedAmount;
|
||||
final String createTime;
|
||||
final int claimStartTimeMs;
|
||||
final int expireTimeMs;
|
||||
|
||||
bool get hasPacketId => packetId.trim().isNotEmpty;
|
||||
|
||||
int get waitingSeconds {
|
||||
if (countdown > 0) {
|
||||
return countdown;
|
||||
}
|
||||
final diff = claimStartTimeMs - DateTime.now().millisecondsSinceEpoch;
|
||||
if (diff <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (diff / 1000).ceil();
|
||||
}
|
||||
|
||||
bool get isWaiting => waitingSeconds > 0;
|
||||
|
||||
bool get isSoldOut {
|
||||
final normalizedStatus = statusDesc.toLowerCase();
|
||||
return remainCount <= 0 ||
|
||||
status == 2 ||
|
||||
normalizedStatus.contains('finish') ||
|
||||
normalizedStatus.contains('sold');
|
||||
}
|
||||
|
||||
bool get isExpired {
|
||||
final normalizedStatus = statusDesc.toLowerCase();
|
||||
if (expireTimeMs > 0 &&
|
||||
DateTime.now().millisecondsSinceEpoch >= expireTimeMs) {
|
||||
return true;
|
||||
}
|
||||
return status == 3 ||
|
||||
normalizedStatus.contains('expire') ||
|
||||
normalizedStatus.contains('expired');
|
||||
}
|
||||
|
||||
bool get isOpenable => !grabbed && !isWaiting && !isSoldOut && !isExpired;
|
||||
|
||||
num get mockClaimAmount {
|
||||
final normalizedCount = totalCount <= 0 ? 1 : totalCount;
|
||||
if (claimedAmount > 0) {
|
||||
return claimedAmount;
|
||||
}
|
||||
final average = totalAmount / normalizedCount;
|
||||
return average <= 0 ? 100000 : average.round();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'packetId': packetId,
|
||||
'userName': senderName,
|
||||
'userAvatar': senderAvatar,
|
||||
'userId': senderId,
|
||||
'totalAmount': totalAmount,
|
||||
'totalCount': totalCount,
|
||||
'remainCount': remainCount,
|
||||
'expireMinutes': expireMinutes,
|
||||
'countdown': countdown,
|
||||
'status': status,
|
||||
'statusDesc': statusDesc,
|
||||
'grabbed': grabbed,
|
||||
'claimedAmount': claimedAmount,
|
||||
'createTime': createTime,
|
||||
'claimStartTime': claimStartTimeMs,
|
||||
'expireTime': expireTimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _decodePayload(String? payload) {
|
||||
final normalizedPayload = (payload ?? '').trim();
|
||||
if (normalizedPayload.isEmpty) {
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(normalizedPayload);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
}
|
||||
if (decoded is Map) {
|
||||
return decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
} catch (_) {}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
static String _stringValue(
|
||||
Map<String, dynamic> json,
|
||||
List<String> keys, {
|
||||
String fallback = '',
|
||||
}) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
final text = value?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static num _numValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
final parsed = num.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _intValue(Map<String, dynamic> json, List<String> keys) {
|
||||
return _numValue(json, keys).round();
|
||||
}
|
||||
|
||||
static int _timestampValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final timestamp = _timestampValueFromDynamic(json[key]);
|
||||
if (timestamp > 0) {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _timestampValueFromDynamic(dynamic value) {
|
||||
if (value is num) {
|
||||
final milliseconds = value.round();
|
||||
return milliseconds < 10000000000 ? milliseconds * 1000 : milliseconds;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
final numeric = num.tryParse(normalized);
|
||||
if (numeric != null) {
|
||||
final milliseconds = numeric.round();
|
||||
return milliseconds < 10000000000 ? milliseconds * 1000 : milliseconds;
|
||||
}
|
||||
return DateTime.tryParse(normalized)?.millisecondsSinceEpoch ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool _boolValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.toLowerCase().trim();
|
||||
if (normalized == 'true' || normalized == '1') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == '0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,462 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_error.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
|
||||
class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
const RoomRedPacketOpenDialog({super.key, required this.data});
|
||||
|
||||
static const String dialogTag = 'showRoomRedPacketOpenDialog';
|
||||
|
||||
final RoomRedPacketUiData data;
|
||||
|
||||
static void show(BuildContext context, RoomRedPacketUiData data) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black.withValues(alpha: 0.55),
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => RoomRedPacketOpenDialog(data: data),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomRedPacketOpenDialog> createState() =>
|
||||
_RoomRedPacketOpenDialogState();
|
||||
}
|
||||
|
||||
class _RoomRedPacketOpenDialogState extends State<RoomRedPacketOpenDialog> {
|
||||
late _RoomRedPacketOpenState _state;
|
||||
bool _claiming = false;
|
||||
num? _claimAmount;
|
||||
|
||||
bool get _usePendingBackground =>
|
||||
_state == _RoomRedPacketOpenState.ready ||
|
||||
_state == _RoomRedPacketOpenState.waiting;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_state = _initialState(widget.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 红包弹层依赖整张位图背景,下面这些定位值按设计图拆开集中管理:
|
||||
// 未打开态素材是 472x602,对应设计稿里约 236x301 的显示比例;
|
||||
// 后续如果还要对稿,优先只微调 avatarTop / senderNameTop /
|
||||
// centerContentTop / actionBottom,不要改动各层级顺序。
|
||||
final dialogWidth = (_usePendingBackground ? 236 : 244).w;
|
||||
final dialogHeight = (_usePendingBackground ? 301 : 322).w;
|
||||
final avatarSize = (_usePendingBackground ? 42 : 66).w;
|
||||
final avatarTop = (_usePendingBackground ? 32 : -18).w;
|
||||
final senderNameTop = (_usePendingBackground ? 86 : 64).w;
|
||||
final centerContentTop = (_usePendingBackground ? 104 : 100).w;
|
||||
final actionBottom = (_usePendingBackground ? 62 : 58).w;
|
||||
return SizedBox(
|
||||
width: ScreenUtil().screenWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: dialogWidth,
|
||||
height: dialogHeight,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
_usePendingBackground
|
||||
? _RoomRedPacketOpenAssets.pendingBackground
|
||||
: _RoomRedPacketOpenAssets.background,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(top: avatarTop, child: _buildAvatar(avatarSize)),
|
||||
Positioned(
|
||||
top: senderNameTop,
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
child: _buildSenderName(),
|
||||
),
|
||||
Positioned(
|
||||
top: centerContentTop,
|
||||
left: 24.w,
|
||||
right: 24.w,
|
||||
child: _buildCenterContent(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: actionBottom,
|
||||
child:
|
||||
_state == _RoomRedPacketOpenState.ready
|
||||
? _buildOpenButton(enabled: true)
|
||||
: _buildStateButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
SCDebounceWidget(
|
||||
onTap:
|
||||
() =>
|
||||
SmartDialog.dismiss(tag: RoomRedPacketOpenDialog.dialogTag),
|
||||
child: Image.asset(
|
||||
_RoomRedPacketOpenAssets.close,
|
||||
width: 34.w,
|
||||
height: 34.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(double size) {
|
||||
if (widget.data.senderAvatar.trim().isNotEmpty) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2.w),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: netImage(
|
||||
url: widget.data.senderAvatar,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
border: Border.all(color: Colors.white, width: 2.w),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
_RoomRedPacketOpenAssets.smallPacket,
|
||||
width: size * 0.58,
|
||||
height: size * 0.58,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSenderName() {
|
||||
final name =
|
||||
widget.data.senderName.trim().isEmpty
|
||||
? SCAppLocalizations.of(context)!.redEnvelope
|
||||
: widget.data.senderName.trim();
|
||||
return Text(
|
||||
name,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFA2352D),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCenterContent() {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
switch (_state) {
|
||||
case _RoomRedPacketOpenState.ready:
|
||||
return _buildReadyContent(localizations);
|
||||
case _RoomRedPacketOpenState.opened:
|
||||
return _buildOpenedContent(localizations);
|
||||
case _RoomRedPacketOpenState.waiting:
|
||||
return _buildMessageContent(
|
||||
localizations.roomRedPacketOpenWaiting(
|
||||
_waitingMinutes(widget.data).toString(),
|
||||
),
|
||||
);
|
||||
case _RoomRedPacketOpenState.soldOut:
|
||||
return _buildMessageContent(localizations.roomRedPacketOpenSoldOut);
|
||||
case _RoomRedPacketOpenState.expired:
|
||||
return _buildMessageContent(localizations.roomRedPacketOpenExpired);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildReadyContent(SCAppLocalizations localizations) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: 6.w),
|
||||
Text(
|
||||
localizations.roomRedPacketOpenSubtitle,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF8B5B35),
|
||||
fontSize: 12.sp,
|
||||
height: 1.25,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
_formatNumber(widget.data.totalAmount.round()),
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFA2352D),
|
||||
fontSize: 23.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOpenedContent(SCAppLocalizations localizations) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: 2.w),
|
||||
Text(
|
||||
localizations.roomRedPacketOpenYouGot,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF8B5B35),
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 13.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
_RoomRedPacketOpenAssets.amountIcon,
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_formatNumber(
|
||||
(_claimAmount ?? widget.data.mockClaimAmount).round(),
|
||||
),
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFA2352D),
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageContent(String message) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 26.w),
|
||||
child: Text(
|
||||
message,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF8B5B35),
|
||||
fontSize: 12.sp,
|
||||
height: 1.3,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOpenButton({required bool enabled}) {
|
||||
return Center(
|
||||
child: SCDebounceWidget(
|
||||
onTap: enabled ? _claimRedPacket : () {},
|
||||
child: Image.asset(
|
||||
enabled
|
||||
? _RoomRedPacketOpenAssets.openEnabled
|
||||
: _RoomRedPacketOpenAssets.openDisabled,
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStateButton() {
|
||||
if (_state == _RoomRedPacketOpenState.waiting) {
|
||||
return _buildOpenButton(enabled: false);
|
||||
}
|
||||
return Center(
|
||||
child: SCDebounceWidget(
|
||||
onTap:
|
||||
() => SmartDialog.dismiss(tag: RoomRedPacketOpenDialog.dialogTag),
|
||||
child: Container(
|
||||
width: 154.w,
|
||||
height: 32.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage(_RoomRedPacketOpenAssets.sheetSendButton),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18.w),
|
||||
),
|
||||
child: Text(
|
||||
SCAppLocalizations.of(context)!.roomRedPacketOkay,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_RoomRedPacketOpenState _initialState(RoomRedPacketUiData data) {
|
||||
if (data.grabbed) {
|
||||
return _RoomRedPacketOpenState.opened;
|
||||
}
|
||||
if (data.isExpired) {
|
||||
return _RoomRedPacketOpenState.expired;
|
||||
}
|
||||
if (data.isSoldOut) {
|
||||
return _RoomRedPacketOpenState.soldOut;
|
||||
}
|
||||
if (data.isWaiting) {
|
||||
return _RoomRedPacketOpenState.waiting;
|
||||
}
|
||||
return _RoomRedPacketOpenState.ready;
|
||||
}
|
||||
|
||||
int _waitingMinutes(RoomRedPacketUiData data) {
|
||||
final waitingSeconds = data.waitingSeconds;
|
||||
if (waitingSeconds > 0) {
|
||||
return (waitingSeconds / 60).ceil();
|
||||
}
|
||||
return data.expireMinutes.round();
|
||||
}
|
||||
|
||||
Future<void> _claimRedPacket() async {
|
||||
if (_claiming) {
|
||||
return;
|
||||
}
|
||||
if (_isLocalPreviewPacket) {
|
||||
setState(() {
|
||||
_claimAmount = widget.data.mockClaimAmount;
|
||||
_state = _RoomRedPacketOpenState.opened;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() => _claiming = true);
|
||||
try {
|
||||
final result = await SCChatRoomRepository().roomRedPacketGrab(
|
||||
widget.data.packetId,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_claimAmount = result.amount ?? widget.data.mockClaimAmount;
|
||||
_state = _RoomRedPacketOpenState.opened;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final code = roomRedPacketErrorCode(error);
|
||||
setState(() {
|
||||
if (code == 'voice_room_red_packet_not_started') {
|
||||
_state = _RoomRedPacketOpenState.waiting;
|
||||
} else if (code == 'voice_room_red_packet_expired') {
|
||||
_state = _RoomRedPacketOpenState.expired;
|
||||
} else if (code == 'voice_room_red_packet_finished') {
|
||||
_state = _RoomRedPacketOpenState.soldOut;
|
||||
}
|
||||
});
|
||||
SCTts.show(roomRedPacketErrorMessage(context, error));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _claiming = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isLocalPreviewPacket {
|
||||
final packetId = widget.data.packetId.trim();
|
||||
return packetId.isEmpty ||
|
||||
packetId.startsWith('mock-') ||
|
||||
packetId.startsWith('test-');
|
||||
}
|
||||
|
||||
String _formatNumber(int value) {
|
||||
final raw = value.toString();
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
if (i > 0 && (raw.length - i) % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(raw[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
enum _RoomRedPacketOpenState { ready, opened, waiting, soldOut, expired }
|
||||
|
||||
class _RoomRedPacketOpenAssets {
|
||||
static const String _base = 'sc_images/room/red_packet';
|
||||
static const String background = '$_base/open_dialog_bg.png';
|
||||
static const String pendingBackground = '$_base/open_pending_bg.png';
|
||||
static const String close = '$_base/close.png';
|
||||
static const String openEnabled = '$_base/open_enabled.png';
|
||||
static const String openDisabled = '$_base/open_disabled.png';
|
||||
static const String smallPacket = '$_base/small_packet_icon.png';
|
||||
static const String amountIcon = '$_base/sheet_amount_icon.png';
|
||||
static const String sheetSendButton = '$_base/sheet_send_button.png';
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
|
||||
class RoomRedPacketRulePage extends StatelessWidget {
|
||||
const RoomRedPacketRulePage({super.key});
|
||||
|
||||
static const Color _backgroundColor = Color(0xFF052821);
|
||||
static const Color _textColor = Colors.white;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
backgroundColor: _backgroundColor,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_RoomRedPacketRuleTopBar(
|
||||
title: localizations.roomRedPacketRulesTitle,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(28.w, 18.w, 28.w, 32.w),
|
||||
child: Text(
|
||||
localizations.roomRedPacketRulesContent,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: _textColor,
|
||||
fontSize: 14.sp,
|
||||
height: 1.36,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketRuleTopBar extends StatelessWidget {
|
||||
const _RoomRedPacketRuleTopBar({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textDirection = Directionality.of(context);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 58.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 22.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: SizedBox(
|
||||
width: 36.w,
|
||||
height: 58.w,
|
||||
child: Icon(
|
||||
textDirection == TextDirection.rtl
|
||||
? Icons.chevron_right
|
||||
: Icons.chevron_left,
|
||||
color: Colors.white,
|
||||
size: 31.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 58.w),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,619 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_error.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_history_page.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_rule_page.dart';
|
||||
|
||||
class RoomRedPacketSendPanel extends StatefulWidget {
|
||||
const RoomRedPacketSendPanel({
|
||||
super.key,
|
||||
this.onSend,
|
||||
this.onHistoryTap,
|
||||
this.onRuleTap,
|
||||
});
|
||||
|
||||
static const String dialogTag = 'showRoomRedPacketSendPanel';
|
||||
|
||||
final ValueChanged<RoomRedPacketDraft>? onSend;
|
||||
final VoidCallback? onHistoryTap;
|
||||
final VoidCallback? onRuleTap;
|
||||
|
||||
static void show(BuildContext context) {
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: true,
|
||||
builder: (_) => const RoomRedPacketSendPanel(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomRedPacketSendPanel> createState() => _RoomRedPacketSendPanelState();
|
||||
}
|
||||
|
||||
class RoomRedPacketDraft {
|
||||
const RoomRedPacketDraft({
|
||||
required this.totalAmount,
|
||||
required this.totalCount,
|
||||
required this.availableAfterMinutes,
|
||||
});
|
||||
|
||||
final int totalAmount;
|
||||
final int totalCount;
|
||||
final int availableAfterMinutes;
|
||||
|
||||
bool get claimNow => availableAfterMinutes <= 0;
|
||||
}
|
||||
|
||||
class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
||||
// 弹窗总高度:只影响整块底部弹窗从屏幕底部往上露出的高度。
|
||||
static const double _panelHeight = 520;
|
||||
|
||||
// 主背景贴图尺寸:对应“红包底部弹窗背景.png”,贴底铺开。
|
||||
static const double _sheetBgHeight = 481;
|
||||
|
||||
// 顶部三红包贴图尺寸与位置:数值越小,顶部红包越靠上、越不挡内容区。
|
||||
static const double _headerTop = 25;
|
||||
static const double _headerWidth = 232;
|
||||
static const double _headerHeight = 174;
|
||||
|
||||
// 内容区位置:控制三行选项和发送按钮整体上移/下移。
|
||||
static const double _contentTop = 182;
|
||||
static const double _contentHorizontalPadding = 24;
|
||||
|
||||
// 选项行尺寸:行高和行间距越小,底部红色余额栏越不容易遮住发送按钮。
|
||||
static const double _configRowHeight = 48;
|
||||
static const double _configRowGap = 12;
|
||||
static const double _sendButtonTopGap = 12;
|
||||
|
||||
// 底部余额栏尺寸:这层贴图放在内容层下面,避免盖住发送按钮。
|
||||
static const double _footerHeight = 92;
|
||||
static const double _footerStartPadding = 28;
|
||||
static const double _footerEndPadding = 14;
|
||||
static const double _footerTopPadding = 30;
|
||||
static const double _footerAmountFontSize = 22;
|
||||
|
||||
static const List<int> _defaultAmountOptions = [15000, 30000, 70000, 100000];
|
||||
static const List<int> _defaultCountOptions = [5, 10, 20, 50];
|
||||
static const List<_PickupOption> _defaultPickupOptions = [
|
||||
_PickupOption(minutes: 5),
|
||||
_PickupOption(minutes: 0),
|
||||
];
|
||||
|
||||
List<int> _amountOptions = List<int>.of(_defaultAmountOptions);
|
||||
List<int> _countOptions = List<int>.of(_defaultCountOptions);
|
||||
List<_PickupOption> _pickupOptions = List<_PickupOption>.of(
|
||||
_defaultPickupOptions,
|
||||
);
|
||||
int _selectedAmount = _defaultAmountOptions.first;
|
||||
int _selectedCount = _defaultCountOptions.first;
|
||||
int _selectedPickupMinutes = _defaultPickupOptions.first.minutes;
|
||||
bool _redPacketEnabled = true;
|
||||
bool _isSending = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConfig();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: _panelHeight.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Positioned(
|
||||
top: _headerTop.w,
|
||||
child: Image.asset(
|
||||
_RoomRedPacketAssets.sheetHeader,
|
||||
width: _headerWidth.w,
|
||||
height: _headerHeight.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: Image.asset(
|
||||
_RoomRedPacketAssets.sheetBg,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: _sheetBgHeight.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(left: 0, right: 0, bottom: 0, child: _buildFooter()),
|
||||
PositionedDirectional(
|
||||
end: 32.w,
|
||||
top: 150.w,
|
||||
child: _buildIconAction(
|
||||
asset: _RoomRedPacketAssets.ruleIcon,
|
||||
onTap: widget.onRuleTap ?? _openRulePage,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: _contentHorizontalPadding.w,
|
||||
end: _contentHorizontalPadding.w,
|
||||
top: _contentTop.w,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildAmountRow(localizations),
|
||||
SizedBox(height: _configRowGap.w),
|
||||
_buildQuantityRow(localizations),
|
||||
SizedBox(height: _configRowGap.w),
|
||||
_buildPickupRow(localizations),
|
||||
SizedBox(height: _sendButtonTopGap.w),
|
||||
_buildSendButton(localizations),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountRow(SCAppLocalizations localizations) {
|
||||
return _buildConfigRow(
|
||||
icon: _RoomRedPacketAssets.amountIcon,
|
||||
title: localizations.roomRedPacketGoldCoins,
|
||||
children:
|
||||
_amountOptions.map((amount) {
|
||||
return _buildOptionButton(
|
||||
label: _formatPresetAmount(amount),
|
||||
selected: amount == _selectedAmount,
|
||||
width: amount >= 100000 ? 50.w : 40.w,
|
||||
onTap: () => setState(() => _selectedAmount = amount),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityRow(SCAppLocalizations localizations) {
|
||||
return _buildConfigRow(
|
||||
icon: _RoomRedPacketAssets.quantityIcon,
|
||||
title: localizations.roomRedPacketQuantity,
|
||||
children:
|
||||
_countOptions.map((count) {
|
||||
return _buildOptionButton(
|
||||
label: '$count',
|
||||
selected: count == _selectedCount,
|
||||
width: 40.w,
|
||||
onTap: () => setState(() => _selectedCount = count),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickupRow(SCAppLocalizations localizations) {
|
||||
return _buildConfigRow(
|
||||
icon: _RoomRedPacketAssets.countdownIcon,
|
||||
title: localizations.roomRedPacketPickupTime,
|
||||
children:
|
||||
_pickupOptions.map((option) {
|
||||
return _buildOptionButton(
|
||||
label: _pickupOptionLabel(localizations, option),
|
||||
selected: option.minutes == _selectedPickupMinutes,
|
||||
width: option.minutes == 0 ? 68.w : 82.w,
|
||||
onTap:
|
||||
() => setState(() => _selectedPickupMinutes = option.minutes),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfigRow({
|
||||
required String icon,
|
||||
required String title,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return Container(
|
||||
height: _configRowHeight.w,
|
||||
padding: EdgeInsetsDirectional.only(start: 15.w, end: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.78),
|
||||
borderRadius: BorderRadius.circular((_configRowHeight / 2).w),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFEFCF83).withValues(alpha: 0.45),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(icon, width: 24.w, height: 24.w, fit: BoxFit.contain),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF94302C),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < children.length; i++) ...[
|
||||
if (i > 0) SizedBox(width: 6.w),
|
||||
children[i],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionButton({
|
||||
required String label,
|
||||
required bool selected,
|
||||
required double width,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SCDebounceWidget(
|
||||
debounceTime: const Duration(milliseconds: 120),
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: 28.w,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
selected
|
||||
? _RoomRedPacketAssets.optionSelected
|
||||
: _RoomRedPacketAssets.optionUnselected,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : const Color(0xFFFF3A2F),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(SCAppLocalizations localizations) {
|
||||
return SCDebounceWidget(
|
||||
onTap: _sendRedPacket,
|
||||
child: SizedBox(
|
||||
width: 230.w,
|
||||
height: 52.w,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(_RoomRedPacketAssets.sendButton, fit: BoxFit.fill),
|
||||
Center(
|
||||
child: Text(
|
||||
_isSending
|
||||
? localizations.roomRedPacketSending
|
||||
: localizations.roomRedPacketSendLuckyPack,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return SizedBox(
|
||||
height: _footerHeight.w,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset(_RoomRedPacketAssets.sheetFooter, fit: BoxFit.fill),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: _footerStartPadding.w,
|
||||
end: _footerEndPadding.w,
|
||||
top: _footerTopPadding.w,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
_RoomRedPacketAssets.amountIcon,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Flexible(
|
||||
child: Selector<SocialChatUserProfileManager, double>(
|
||||
selector: (_, manager) => manager.myBalance,
|
||||
builder: (context, balance, child) {
|
||||
return Text(
|
||||
_formatNumber(balance.round()),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: _footerAmountFontSize.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Icon(
|
||||
Directionality.of(context) == TextDirection.rtl
|
||||
? Icons.chevron_left
|
||||
: Icons.chevron_right,
|
||||
size: 26.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildIconAction(
|
||||
asset: _RoomRedPacketAssets.historyIcon,
|
||||
onTap: widget.onHistoryTap ?? _openHistoryPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconAction({
|
||||
required String asset,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SCDebounceWidget(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
asset,
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadConfig() async {
|
||||
try {
|
||||
final config = await SCChatRoomRepository().roomRedPacketConfig();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final nextAmountOptions =
|
||||
config.amountOptions.isNotEmpty
|
||||
? config.amountOptions
|
||||
: _defaultAmountOptions;
|
||||
final nextCountOptions =
|
||||
config.countOptions.isNotEmpty
|
||||
? config.countOptions
|
||||
: _defaultCountOptions;
|
||||
final delayMinutes = (config.delaySeconds / 60).ceil();
|
||||
final nextPickupOptions = <_PickupOption>[
|
||||
if (delayMinutes > 0) _PickupOption(minutes: delayMinutes),
|
||||
const _PickupOption(minutes: 0),
|
||||
];
|
||||
setState(() {
|
||||
_redPacketEnabled = config.enabled;
|
||||
_amountOptions = List<int>.of(nextAmountOptions);
|
||||
_countOptions = List<int>.of(nextCountOptions);
|
||||
_pickupOptions =
|
||||
nextPickupOptions.isNotEmpty
|
||||
? nextPickupOptions
|
||||
: List<_PickupOption>.of(_defaultPickupOptions);
|
||||
if (!_amountOptions.contains(_selectedAmount)) {
|
||||
_selectedAmount = _amountOptions.first;
|
||||
}
|
||||
if (!_countOptions.contains(_selectedCount)) {
|
||||
_selectedCount = _countOptions.first;
|
||||
}
|
||||
if (!_pickupOptions.any(
|
||||
(option) => option.minutes == _selectedPickupMinutes,
|
||||
)) {
|
||||
_selectedPickupMinutes = _pickupOptions.first.minutes;
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
// 配置接口失败时继续用本地默认档位,避免发送面板不可用。
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendRedPacket() async {
|
||||
if (_isSending) {
|
||||
return;
|
||||
}
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
if (!_redPacketEnabled) {
|
||||
SCTts.show(localizations.roomRedPacketComingSoon);
|
||||
return;
|
||||
}
|
||||
widget.onSend?.call(
|
||||
RoomRedPacketDraft(
|
||||
totalAmount: _selectedAmount,
|
||||
totalCount: _selectedCount,
|
||||
availableAfterMinutes: _selectedPickupMinutes,
|
||||
),
|
||||
);
|
||||
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final roomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
if (roomId.trim().isEmpty) {
|
||||
SCTts.show(localizations.roomRedPacketPresenceRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isSending = true);
|
||||
try {
|
||||
final result = await SCChatRoomRepository().roomSendRedPacket(
|
||||
roomId,
|
||||
_selectedAmount.toString(),
|
||||
_selectedCount.toString(),
|
||||
_selectedPickupMinutes.toString(),
|
||||
packetMode: _selectedPickupMinutes <= 0 ? 'IMMEDIATE' : 'DELAYED',
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final balanceAfter = result.balanceAfter;
|
||||
if (balanceAfter != null) {
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).updateBalance(balanceAfter.toDouble());
|
||||
}
|
||||
rtcProvider.loadRoomRedPacketList(1).catchError((_) {
|
||||
return rtcProvider.redPacketList;
|
||||
});
|
||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||
SCTts.show(localizations.roomRedPacketSendSuccess);
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
SCTts.show(roomRedPacketErrorMessage(context, error));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isSending = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _openRulePage() {
|
||||
final navigator = navigatorKey.currentState;
|
||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||
if (navigator == null) {
|
||||
return;
|
||||
}
|
||||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||||
navigator.push(
|
||||
MaterialPageRoute(builder: (_) => const RoomRedPacketRulePage()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _openHistoryPage() {
|
||||
final navigator = navigatorKey.currentState;
|
||||
SmartDialog.dismiss(tag: RoomRedPacketSendPanel.dialogTag);
|
||||
if (navigator == null) {
|
||||
return;
|
||||
}
|
||||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||||
navigator.push(
|
||||
MaterialPageRoute(builder: (_) => const RoomRedPacketHistoryPage()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _pickupOptionLabel(
|
||||
SCAppLocalizations localizations,
|
||||
_PickupOption option,
|
||||
) {
|
||||
if (option.minutes <= 0) {
|
||||
return localizations.roomRedPacketClaimNow;
|
||||
}
|
||||
return localizations.roomRedPacketInMinutes(option.minutes.toString());
|
||||
}
|
||||
|
||||
String _formatPresetAmount(int value) {
|
||||
if (value % 1000 == 0) {
|
||||
return '${value ~/ 1000}k';
|
||||
}
|
||||
return _formatNumber(value);
|
||||
}
|
||||
|
||||
String _formatNumber(int value) {
|
||||
final raw = value.toString();
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
if (i > 0 && (raw.length - i) % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(raw[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _PickupOption {
|
||||
const _PickupOption({required this.minutes});
|
||||
|
||||
final int minutes;
|
||||
}
|
||||
|
||||
class _RoomRedPacketAssets {
|
||||
static const String _base = 'sc_images/room/red_packet';
|
||||
static const String sheetBg = '$_base/sheet_bg.png';
|
||||
static const String sheetHeader = '$_base/sheet_header.png';
|
||||
static const String sheetFooter = '$_base/sheet_footer.png';
|
||||
static const String amountIcon = '$_base/sheet_amount_icon.png';
|
||||
static const String quantityIcon = '$_base/sheet_quantity_icon.png';
|
||||
static const String countdownIcon = '$_base/sheet_countdown_icon.png';
|
||||
static const String historyIcon = '$_base/sheet_history_icon.png';
|
||||
static const String ruleIcon = '$_base/sheet_rule_icon.png';
|
||||
static const String sendButton = '$_base/sheet_send_button.png';
|
||||
static const String optionSelected = '$_base/option_large_selected.png';
|
||||
static const String optionUnselected = '$_base/option_large_unselected.png';
|
||||
}
|
||||
@ -36,12 +36,20 @@ class RoomGameEntryButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class RoomGameBottomSheet extends StatelessWidget {
|
||||
const RoomGameBottomSheet({super.key, required this.roomContext});
|
||||
const RoomGameBottomSheet({
|
||||
super.key,
|
||||
required this.roomContext,
|
||||
this.initialGameId,
|
||||
});
|
||||
|
||||
final BuildContext roomContext;
|
||||
final String? initialGameId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoomGameListSheet(roomContext: roomContext);
|
||||
return RoomGameListSheet(
|
||||
roomContext: roomContext,
|
||||
initialGameId: initialGameId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart';
|
||||
import '../../../app/routes/sc_fluro_navigator.dart';
|
||||
import '../../../modules/index/main_route.dart';
|
||||
import '../../../modules/room/voice_room_route.dart';
|
||||
@ -38,6 +39,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
static const int _menuReport = 5;
|
||||
static const int _menuBackground = 6;
|
||||
static const int _menuMusic = 7;
|
||||
static const int _menuRedPacket = 8;
|
||||
static bool get _showBackgroundFeature => true;
|
||||
|
||||
List<RoomMenu> items2 = [];
|
||||
@ -94,6 +96,13 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
"sc_icon_room_msg_pic.png",
|
||||
),
|
||||
);
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
_menuRedPacket,
|
||||
SCAppLocalizations.of(context)!.redEnvelope,
|
||||
"red_packet/menu_red_packet_icon.png",
|
||||
),
|
||||
);
|
||||
if (Provider.of<RtcProvider>(context, listen: false).isFz()) {
|
||||
items2.add(
|
||||
RoomMenu(
|
||||
@ -263,6 +272,9 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
}, neeCrop: false);
|
||||
}
|
||||
} else if (item.id == _menuRedPacket) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
RoomRedPacketSendPanel.show(navigatorKey.currentState!.context);
|
||||
} else if (item.id == _menuSettings) {
|
||||
SmartDialog.dismiss(tag: "showRoomMenuDialog");
|
||||
VoiceRoomRoute.openRoomEdit(
|
||||
@ -294,7 +306,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
}
|
||||
|
||||
Widget _buildMenuIcon(RoomMenu item) {
|
||||
if (item.id != _menuMusic) {
|
||||
if (item.id != _menuMusic && item.id != _menuRedPacket) {
|
||||
return Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 45.w,
|
||||
@ -311,8 +323,8 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
"sc_images/room/${item.icon}",
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
width: item.id == _menuRedPacket ? 24.w : 26.w,
|
||||
height: item.id == _menuRedPacket ? 28.w : 26.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
@ -20,7 +21,8 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart';
|
||||
@ -113,6 +115,11 @@ class _MsgItemState extends State<MsgItem> {
|
||||
if (widget.msg.type == SCRoomMsgType.roomLuckNumber) {
|
||||
return _buildLuckNumberMsg(context);
|
||||
}
|
||||
|
||||
///房间红包
|
||||
if (widget.msg.type == SCRoomMsgType.roomRedPacket) {
|
||||
return _buildRoomRedPacketMsg(context);
|
||||
}
|
||||
if (widget.msg.user != null) {
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
@ -211,6 +218,35 @@ class _MsgItemState extends State<MsgItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoomRedPacketMsg(BuildContext context) {
|
||||
final data = RoomRedPacketUiData.fromMessagePayload(
|
||||
payload: widget.msg.msg,
|
||||
fallbackUser: widget.msg.user,
|
||||
);
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width(5.w),
|
||||
vertical: width(4),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: width(10),
|
||||
).copyWith(bottom: 6.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.msg.user != null) ...[
|
||||
_buildUserInfoLine(context),
|
||||
SizedBox(height: 6.w),
|
||||
],
|
||||
RoomRedPacketChatMessage(data: data),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVipTextMessageBubble(PropsResources? resource) {
|
||||
final messageText = widget.msg.msg ?? "";
|
||||
final textStyle = TextStyle(
|
||||
@ -272,7 +308,7 @@ class _MsgItemState extends State<MsgItem> {
|
||||
url: imageUrl,
|
||||
width: bubbleWidth,
|
||||
height: bubbleHeight,
|
||||
fallback: _buildVipBubbleFallbackBackground(),
|
||||
fallback: SizedBox(width: bubbleWidth, height: bubbleHeight),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
|
||||
@ -314,11 +350,11 @@ class _MsgItemState extends State<MsgItem> {
|
||||
);
|
||||
final bubbleWidth = math.min(
|
||||
maxBubbleWidth,
|
||||
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
|
||||
math.max(minSkinWidth, textSize.width + horizontalPadding * 2),
|
||||
);
|
||||
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
|
||||
final bubbleHeight = math.max(
|
||||
verticalPadding * 2,
|
||||
minSkinHeight,
|
||||
textSize.height + verticalPadding * 2,
|
||||
);
|
||||
|
||||
@ -330,7 +366,6 @@ class _MsgItemState extends State<MsgItem> {
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(child: _buildVipBubbleFallbackBackground()),
|
||||
Positioned.fill(
|
||||
child: _buildVipBubbleSkin(
|
||||
url: imageUrl,
|
||||
@ -385,16 +420,10 @@ class _MsgItemState extends State<MsgItem> {
|
||||
fit: BoxFit.fill,
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
)
|
||||
: SCNinePatchImage(
|
||||
: _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
),
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
),
|
||||
);
|
||||
@ -415,15 +444,44 @@ class _MsgItemState extends State<MsgItem> {
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
return SCNinePatchImage(
|
||||
return _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStaticVipBubbleImage({
|
||||
required String url,
|
||||
required double width,
|
||||
required double height,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final imageUrl = url.trim();
|
||||
if (imageUrl.isEmpty) {
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
}
|
||||
|
||||
return Image(
|
||||
image: buildCachedImageProvider(
|
||||
imageUrl,
|
||||
logicalWidth: width,
|
||||
logicalHeight: height,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
debugPrint(
|
||||
'[VIP][RoomChatBubble] image load failed url=$imageUrl error=$error',
|
||||
);
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Size _measureMessageTextSize({
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
@ -436,25 +494,18 @@ class _MsgItemState extends State<MsgItem> {
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
Widget _buildVipBubbleFallbackBackground() {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.38),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(18.w),
|
||||
bottomLeft: Radius.circular(18.w),
|
||||
bottomRight: Radius.circular(18.w),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveRoomChatBubbleImageUrl(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.roomNotOpenedUrl,
|
||||
resource?.imSendCoverUrl,
|
||||
resource?.imOpenedUrl,
|
||||
resource?.imOpenedUrlTwo,
|
||||
resource?.imNotOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.expand,
|
||||
resource?.sourceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -218,6 +218,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
ref.userCardInfo?.userProfile
|
||||
?.getHeaddress()
|
||||
?.sourceUrl,
|
||||
headdressCover:
|
||||
ref.userCardInfo?.userProfile
|
||||
?.getHeaddress()
|
||||
?.cover,
|
||||
),
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
|
||||
@ -82,6 +82,7 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
user?.userNickname ?? "",
|
||||
user?.roles ?? "",
|
||||
user?.getHeaddress()?.sourceUrl ?? "",
|
||||
user?.getHeaddress()?.cover ?? "",
|
||||
user?.getVIP()?.name ?? "",
|
||||
);
|
||||
}
|
||||
@ -98,7 +99,7 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
configuredSeatCount:
|
||||
provider.currenRoom?.roomProfile?.roomSetting?.mikeSize
|
||||
?.toInt() ??
|
||||
provider.previewRoomSeatCount ??
|
||||
provider.previewRoomSeatCount ??
|
||||
0,
|
||||
hasCurrentRoom: provider.currenRoom != null,
|
||||
isExitingCurrentVoiceRoomSession:
|
||||
|
||||
@ -70,6 +70,7 @@ class _PropsStoreHeaddressDetailDialogState
|
||||
widget.res.propsResources?.sourceUrl ??
|
||||
""
|
||||
"",
|
||||
headdressCover: widget.res.propsResources?.cover,
|
||||
),
|
||||
SizedBox(height: 25.w),
|
||||
Row(
|
||||
|
||||
562
local_packages/flutter_svga-0.0.13-patched/pubspec.lock
Normal file
@ -0,0 +1,562 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.4.0"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_drawing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_drawing
|
||||
sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.4.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.2.7+9
|
||||
version: 1.3.0+10
|
||||
|
||||
|
||||
environment:
|
||||
@ -169,6 +169,7 @@ flutter:
|
||||
- sc_images/room/background_examples/
|
||||
- sc_images/room/emoji/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/red_packet/
|
||||
- sc_images/room/anim/
|
||||
- sc_images/room/anim/gift/
|
||||
- sc_images/room/anim/luck_gift/
|
||||
|
||||
BIN
sc_images/room/red_packet/close.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
sc_images/room/red_packet/floating_go.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
sc_images/room/red_packet/menu_red_packet_icon.png
Normal file
|
After Width: | Height: | Size: 750 B |
BIN
sc_images/room/red_packet/open_dialog_bg.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
sc_images/room/red_packet/open_disabled.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
sc_images/room/red_packet/open_enabled.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
sc_images/room/red_packet/open_pending_bg.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
sc_images/room/red_packet/option_large_selected.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
sc_images/room/red_packet/option_large_unselected.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
sc_images/room/red_packet/option_small_selected.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
sc_images/room/red_packet/option_small_unselected.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sc_images/room/red_packet/sheet_amount_icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
sc_images/room/red_packet/sheet_bg.png
Normal file
|
After Width: | Height: | Size: 921 KiB |
BIN
sc_images/room/red_packet/sheet_countdown_icon.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
sc_images/room/red_packet/sheet_footer.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
sc_images/room/red_packet/sheet_header.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
sc_images/room/red_packet/sheet_history_icon.png
Normal file
|
After Width: | Height: | Size: 906 B |
BIN
sc_images/room/red_packet/sheet_quantity_icon.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
sc_images/room/red_packet/sheet_rule_icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
sc_images/room/red_packet/sheet_send_button.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
sc_images/room/red_packet/small_packet_icon.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
19
需求进度.md
@ -4,6 +4,21 @@
|
||||
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
|
||||
- 按桌面《Android音乐添加流程.md》在 Android 语言房内补齐本地 MP3 音乐功能,并持续记录素材接入、扫描添加、列表管理、播放器和房间播放链路进度。
|
||||
|
||||
## 本轮语音房红包功能(进行中)
|
||||
- 已完成第一轮只读摸底:项目内已有房间红包接口声明与实现、`ROOM_RED_PACKET` RTM 收包、红包飘屏、`RtcProvider.redPacketList` 房间红包列表缓存、以及被注释的房间菜单红包入口;当前缺口集中在语音房内完整 UI、发送入口、聊天红包消息展示、打开/领取弹层和进入房间后自动打开红包的消费逻辑。
|
||||
- 已确认 `expireMinutes` 业务语义为“多久后可领取”,不是过期时间;本轮在 UI 层会按 `In 5 Minutes / Claim Now` 作为领取等待时间选择,后端接口文档出来后再正式对接请求字段。
|
||||
- 第一步已先落 UI 层:导入桌面 `/Users/nigger/Desktop/app素材/红包` 中的红包菜单、底部发送弹窗、选中态按钮、金币/数量/倒计时、历史、规则、发送按钮、小红包和打开弹层相关 PNG,统一放入 `sc_images/room/red_packet/`,并在 `pubspec.yaml` 注册资源目录。
|
||||
- 第一步已新增语音房红包发送底部弹窗 `RoomRedPacketSendPanel`:当前支持金币档位、红包数量、领取等待时间三组本地选择态,底部展示当前缓存金币余额,规则/历史/发送按钮先保留 UI 占位提示,暂不调用后端。
|
||||
- 第一步已恢复房间菜单红包入口:语音房底部菜单新增 `Red Envelope` 图标,点击后打开红包发送底部弹窗;后续继续补聊天区红包消息、打开/领取状态弹层、飘屏样式替换和接口对接。
|
||||
- 已按 2026-05-07 截图反馈修正第一版 UI:菜单红包入口补齐与 Music 一致的圆形半透明容器;红包发送弹窗把贴图层级改为“主背景 -> 底部余额栏 -> 顶部红包 -> 内容区”,避免底部红条盖住发送按钮;弹窗高度、背景高度、顶部红包尺寸、内容区位置、选项行高、底部余额栏高度已集中成带注释常量,方便后续继续微调;底部金额旁已补方向箭头,并新增 `en/ar/tr/bn` 多语言文案与 RTL 方向兼容。
|
||||
- 已按 2026-05-07 15:53 截图反馈继续修正:底部余额栏金币字体缩小、历史图标右侧间距收窄;右上角规则按钮已进入静态规则页,底部历史按钮已进入静态红包发送历史页;两页先使用本地模拟文案/数据,并补齐 `en/ar/tr/bn` 多语言与 RTL 导航方向兼容。
|
||||
- 已开始第二阶段 UI 层:新增房间红包 UI 数据模型,聊天区 `ROOM_RED_PACKET` 消息可渲染设计图同款红色红包气泡并点击打开弹层;新增打开/领取弹层 UI,先支持本地模拟的可打开、已领取、等待、抢完、过期状态;红包飘屏已替换为新素材风格的红色横幅 + `GO` 按钮,并在当前房间内点击时直接弹出红包打开层。
|
||||
- 已修复红包发送底部弹窗规则/历史图标跳转:改为使用全局导航键,并扩大图标热区;同时在房间游戏入口左侧新增 `TEST` 测试按钮,点击后本地模拟发送一条房间红包消息并触发红包飘屏,方便先查看发送后的 UI 效果。
|
||||
- 已按 2026-05-07 16:47/16:52 截图反馈继续对齐:补入新素材 `未打开红包背景.png` 作为未打开态弹层背景;红包飘窗调整为头像、双行文案、右侧红包图标和 `GO` 图标的设计图层级;聊天红包气泡放宽并降低字号,避免英文提示被截断;领取成功弹层内容整体上移,并去掉压到红包口袋上的冗余提示。
|
||||
- 已按后端《语音房红包 Flutter 接口对接文档》开始接口对接:发送/领取/详情/配置/发包记录切到 `/app/voice-room/red-packet/*`;TIM 登录后拉取当前用户语区红包 IM 群并加入,`0/10013` 都按成功处理,红包飘屏只从全服广播群或当前语区红包群解析,并按 `regionCode` 做同语区过滤;进入房间后每 30 秒上报红包 presence,离开/切房时调用 leave;发送面板、领取弹层和历史页已接真实接口,错误码先映射为多语言提示。
|
||||
- 已按 2026-05-07 17:55 截图反馈微调:Rules 页面顶部导航栏改为满宽安全区布局,返回按钮回到左侧并保留 RTL 方向;红包飘窗重算红条、头像、小红包和 `GO` 图层位置/尺寸,文本禁用系统缩放并使用 `FittedBox` 避免 bottom overflow;未打开红包弹层按 472x602 素材比例收窄,并把头像、昵称、说明、金额和 `OPEN` 整体上移,定位常量已集中注释方便继续对稿。
|
||||
- 已按要求移除房间游戏图标左侧的临时 `TEST` 红包 UI 测试按钮,同时删除本地模拟发送红包消息/飘屏的测试方法,保留真实红包消息链路。
|
||||
|
||||
## 本轮个人资料页改版(分步重做)
|
||||
- 已按 2026-05-06 最新反馈回到旧版个人资料页结构上分步处理,不再一次性重做整页。
|
||||
- 第一步已去掉个人资料页加载阶段的骨架屏:`ref.userProfile` 或拉黑状态还没返回时,正文区域改为空占位,不再展示灰色骨架块;资料返回后仍按原来的 `Header + About/Giftwall Tab` 结构渲染。
|
||||
@ -16,7 +31,9 @@
|
||||
- 第八步已继续按 2026-05-07 最新反馈收口:确认 `Wall Of Honors / Gift Wall` 标题直接使用用户替换后的 PNG 素材,不再自绘标题;非本人资料页底部按钮整体上移并扩大底部安全留白,减少被系统 Home Indicator 遮挡;`Gift Wall` 标题点击已进入新的独立礼物墙页面,页面包含返回栏、头像头像框、昵称、Received/Total Value 统计卡、三列礼物列表和底部 `No Further Data Available~` 文案。
|
||||
- 第九步已修正 Gift Wall 详情页初版问题:顶部导航栏补齐满屏宽度,避免返回箭头挤进标题;头像缩小到 `104`,昵称字号改为正常单次缩放;金币展示改用现有单枚金币动效素材;礼物数据模型兼容 `giftConfig/name/amount/price/value/coins/giftQuantity` 等接口字段,避免礼物名和金额接口有值但页面显示 `-- / 0`。
|
||||
- 第十步已为 Gift Wall 详情页补齐礼物名称/金额解析链路:礼物墙接口返回后会再用全局 `/gift/list` 配置表按 `standardId / id / giftCode / giftPhoto` 做二次匹配,补齐缺失的 `giftName / giftCandy / giftIntegral / giftPhoto`,用于单个礼物卡片和 `Total Value` 统计。
|
||||
- 验证进度:`dart format lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 无 error/warning,仅保留旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。
|
||||
- 第十一步已补齐 Gift Wall 礼物动效预览:礼物卡点击时如果礼物 `giftSourceUrl` 是 `.svga`,会在当前页面全屏播放一次 SVGA 动效;播放期间使用页面状态锁防止重复叠加,播放完成或加载失败后自动清理 overlay 状态。
|
||||
- 第十二步已补 Wall Of Honors 独立页面骨架:个人主页 `Wall Of Honors` 标题点击进入 `/main/person/wallOfHonors`;新页面先按无数据状态只展示满宽顶部返回栏、`My Achievements(0)` 标题、`Honors(0) / Event(0)` 两个 tab 和底部空态文案,顶部栏沿用满宽 `Stack` 写法避免返回箭头挤压标题。
|
||||
- 验证进度:`dart format lib/modules/user/profile/profile_wall_of_honors_page.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/profile_wall_of_honors_page.dart lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 无 error/warning,仅保留旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。
|
||||
|
||||
## 本轮 Android 语言房音乐功能(进行中)
|
||||
- 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。
|
||||
|
||||