礼物动画以及多语言翻译修复

This commit is contained in:
NIGGER SLAYER 2026-04-17 15:21:23 +08:00
parent ea99051267
commit 61c670b9ff
33 changed files with 2975 additions and 2083 deletions

View File

@ -1,4 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.2 -Djdk.tls.client.protocols=TLSv1.2 -Djava.net.preferIPv4Stack=true
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
systemProp.https.protocols=TLSv1.2

View File

@ -1,13 +1,13 @@
{
"signInWithGoogle": "سيج إنويسجوغل",
"or": "اور",
"signInWithYourAccount": "سيج ، إن ، فايس ، ويوا كونتي",
"signInWithApple": "سيج إنويس أبل",
"loginRepresentsAgreementTo": "تسجيل الدخول يمثل الموافقة على",
"termsofService": "تيمز بنسيفيتش",
"privaceyPolicy": "بريفاسيبوليس",
"tips": يبس",
"mine": "خاصتي",
"signInWithGoogle": "تسجيل الدخول باستخدام جوجل",
"or": "أو",
"signInWithYourAccount": "تسجيل الدخول باستخدام حسابك",
"signInWithApple": "تسجيل الدخول باستخدام آبل",
"loginRepresentsAgreementTo": "يعني تسجيل الدخول موافقتك على",
"termsofService": "شروط الخدمة",
"privaceyPolicy": "سياسة الخصوصية",
"tips": نبيه",
"mine": "حسابي",
"searchNoDataTips": "أدخل معرف الغرفة أو المستخدم الذي تريد البحث عنه.",
"party": "حفلة",
"event": "حدث",
@ -15,16 +15,16 @@
"sound2": "صوت",
"games": "ألعاب",
"myRoom": "غرفتي",
"other": "آخر",
"other": "أخرى",
"startYourBrandNewJourney": "ابدأ رحلتك الجديدة تمامًا",
"maliciousHarassment": "التحرش الخبيث",
"maliciousHarassment": "التحرش المسيء",
"aboutMe": "عنّي",
"bag": "حقيبة",
"roomEdit": "تحرير الغرفة",
"cancelRoomPassword": "هل أنت متأكد أنك تريد حذف كلمة مرور الغرفة؟",
"roomMemberFee": "رسوم عضو الغرفة",
"blockedList2": "قائمة المحظورين",
"roomTheme2": "موضوع الغرفة",
"roomTheme2": "سمة الغرفة",
"roomPassword": "كلمة مرور الغرفة",
"numberOfMic": "عدد الميكروفونات",
"pleaseEnterContent": "يرجى إدخال المحتوى",
@ -38,14 +38,14 @@
"gameCenter": "مركز الألعاب",
"returnToVoiceChat": "العودة إلى الدردشة الصوتية؟",
"game": "لعبة",
"invite": "يدعو",
"claim": "مطالبة",
"recent": "حديث",
"popularEvents": "الأحداث الشهيرة",
"invite": "دعوة",
"claim": "استلام",
"recent": "الأحدث",
"popularEvents": "الأحداث الرائجة",
"exitGameMode": "الخروج من وضع اللعبة",
"enterTheRoom": "ادخل الغرفة",
"enterTheRoom": "دخول الغرفة",
"inviteGoRoomTips": "دائمًا هنا من أجلك، في المطر أو الشمس. تعال وقل مرحبًا!",
"confirmInviteThisUserToTheRoom": "هل تؤكد دعوة هذا المستخدم (المعرف: {1}) إلى الغرفة؟",
"confirmInviteThisUserToTheRoom": "هل تؤكد دعوة هذا المستخدم (المعرّف: {1}) إلى الغرفة؟",
"complete": "مكتمل",
"copyLink": "نسخ الرابط",
"shareTo": "مشاركة إلى",
@ -55,9 +55,9 @@
"taskNameRoomOwnerSendRedPacket": "مالك الغرفة يرسل مظروفًا أحمر",
"taskNameRoomNewMember": "أعضاء جدد في الغرفة",
"taskNameRoomOwnerSendGiftUser": "مالك الغرفة يرسل هدية",
"taskNameRoomMicUser120Min": "الأعضاء بقضاء 120+ دقيقة على الميكروفون",
"taskNameRoomMicUser60Min": "الأعضاء بقضاء 60+ دقيقة على الميكروفون",
"taskNameRoomMicUser30Min": "الأعضاء بقضاء 30+ دقيقة على الميكروفون",
"taskNameRoomMicUser120Min": "أعضاء أمضوا 120+ دقيقة على الميكروفون",
"taskNameRoomMicUser60Min": "أعضاء أمضوا 60+ دقيقة على الميكروفون",
"taskNameRoomMicUser30Min": "أعضاء أمضوا 30+ دقيقة على الميكروفون",
"taskNamePersonalSendGift": "أرسل هدية لمستخدم",
"taskNameRoomOnlineUserCount": "الأعضاء المتصلون في الغرفة",
"taskNameRoomOwnerInviteMic": "دعوة عضو إلى الميكروفون",
@ -67,25 +67,25 @@
"taskNamePersonalLuckyGiftGold": "العملات المكتسبة من إرسال هدايا الحظ",
"taskNameRoomOwnerMicTime": "مالك الغرفة يتحدث عبر الميكروفون في الغرفة",
"taskNamePersonalActiveInRoom": "كن نشطًا في غرف الآخرين",
"taskNamePersonalGameConsume": "إنفاق في اللعبة",
"taskNamePersonalMicInRoom": "اذهب إلى المايك",
"taskNamePersonalGameConsume": "الإنفاق داخل الألعاب",
"taskNamePersonalMicInRoom": "الصعود إلى الميكروفون",
"dailyCoinBonanzaRules": "قواعد مهرجان العملات اليومي",
"magic": "سحر",
"dailyCoinBonanzaRulesDetail": "1. يمكن إكمال المهام الشخصية اليومية ومهام صاحب الغرفة اليومية مرة واحدة لكل مستخدم يوميًا. يتم إعادة تعيين المهام عند الساعة 00:00:00 بتوقيت السعودية.\n2. يمكن إكمال المهام الشخصية اليومية فقط في غرف الآخرين؛ ويمكن إكمال مهام صاحب الغرفة فقط في غرفتك الخاصة.\n3. يتم احتساب مهام تقديم الهدايا فقط على الهدايا المرسلة في الغرف، وليس الهدايا المرسلة في الخلاصات.\n4. إذا تم إنشاء عدة حسابات باستخدام نفس الجهاز أو نفس بطاقة SIM بأي شكل من الأشكال، يمكن المطالبة بالمكافآت لجميع المهام مرة واحدة فقط.",
"roomOwnerTasks": "مهام صاحب الغرفة",
"personalTasks": "المهام الشخصية",
"noPromptsToday": "لا توجد مطالبات اليوم.",
"noPromptsToday": "لا توجد مهام متاحة اليوم.",
"coins4": "عملات",
"getPaidToRefer": "احصل على أجر عن الإحالة",
"getPaidToRefer": "اكسب من دعوة الأصدقاء",
"forMoreRewardsPleaseCheckTheTaskCenter": "لمزيد من المكافآت، يرجى التحقق من مركز المهام",
"weekStart": "بداية الأسبوع",
"kingQuuen": "ملك-ملكة",
"ramadan": "رمضان",
"like": "مثل",
"kingQuuen": "الملك والملكة",
"ramadan": "رمضان",
"like": "إعجاب",
"updateNow": "تحديث الآن",
"skip2": "تخطي",
"importantReminder": "تذكير مهم",
"discard": خلص",
"discard": جاهل",
"deleteCommentTips": "هل أنت متأكد أنك تريد حذف هذا التعليق؟",
"deleteSuccessful": "تم الحذف بنجاح!",
"itemsLeft": "الأصناف المتبقية",
@ -101,17 +101,17 @@
"catchFirstComment": "التقط التعليق الأول",
"posting": "نشر",
"trend": "اتجاه",
"reply": "إجابة",
"reply": "رد",
"customizedGiftRules": "قواعد الهدايا المخصصة",
"rulesUpload": "القواعد والرفع",
"rulesUpload": "القواعد والرفع",
"customized": "مخصص",
"coins3": "عملات",
"customizedGiftRulesContent": "كيفية الحصول على هديتي المُخصصة:\n\n1. يتم تحديد أهلية المستخدم للحصول على هدية مُخصصة بناءً على مستوى ثروته الحالي.\n\n(1) إذا كان مستوى ثروة المستخدم ≥ 35: يمكن للمستخدم تحميل فيديو واحد فقط للهدية المُخصصة. سنستخدم صورة ملفه الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير على الهدية في قسم \"التخصيص\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفر الهدية في المتجر.\n\n(2) إذا كان مستوى ثروة المستخدم ≥ 45: يمكن للمستخدم تحميل فيديو واحد فقط للهدية المُخصصة. سنستخدم صورة ملفه الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير على الهدية في قسم \"التخصيص\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفر الهدية في المتجر.\n\n2. يمكنك المشاركة في أنشطة مُحددة على التطبيق، وبعد استيفاء الشروط، تواصل معنا عبر قسم \"الرسائل\" ← \"اتصل بنا\". يرجى تزويدنا بلقطات شاشة للنشاط والفيديو الذي ترغب باستخدامه في الهدية المُخصصة. سنستخدم صورة ملفك الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير لها، ثم نُدرجها ضمن قسم \"مُخصصة\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفرها في المتجر.\n\nمدة صلاحية الهدايا المُخصصة وكيفية تمديدها:\nستكون الهدايا المُخصصة الحصرية متاحة لمدة 30 يومًا. ولتمكين المستخدمين المؤثرين والمُلهمين من الاستمرار في الاستمتاع بهذه التجربة الجديدة وعرض أسلوبهم الشخصي، يُمكن لمستخدمي الهدايا المُخصصة تمديد مدة صلاحية جميع هداياهم المُخصصة لمدة 30 يومًا إضافية عن طريق شحن رصيدهم بمبلغ 500 دولار أمريكي شهريًا.",
"clearCache": "مسح الكاش",
"clearCache": "مسح ذاكرة التخزين المؤقت",
"multiple": "متعدد",
"areYouSureYouWantToClearLocalCache": "هل أنت متأكد أنك تريد مسح الذاكرة المؤقتة المحلية؟",
"luckGiftSpecialEffects": "تأثيرات الرسوم المتحركة للهدية السعيدة",
"receivedFromALuckyGift": "تم الاستلام من هدية محظوظة/سحرية",
"luckGiftSpecialEffects": "تأثيرات هدايا الحظ",
"receivedFromALuckyGift": "تم الاستلام من هدية حظ/سحر",
"successfullyRemovedFromTheBlacklist": "تمت الإزالة من القائمة السوداء بنجاح!",
"successfullyAddedToTheBlacklist": "تمت الإضافة إلى القائمة السوداء بنجاح!",
"youAreCurrentlyCPRelationshipPleaseDissolve": "أنت حاليا في علاقة CP. الرجاء حلها أولا.",
@ -122,15 +122,15 @@
"userBlacklist": "قائمة الحظر للمستخدمين",
"redEnvelopeNotYetClaimed": "المظروف الأحمر لم يُستلم بعد",
"redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات",
"followed": بع",
"followed": مت المتابعة",
"newMessage": "رسالة جديدة",
"keep": "يحفظ",
"keep": "الاحتفاظ",
"open": "فتح",
"clearCacheSuccessfully": "تم مسح ذاكرة التخزين المؤقت بنجاح",
"thisUserHasBeenBlacklisted": "تم إدراج هذا المستخدم في القائمة السوداء.",
"thisFeatureIsCurrentlyUnavailable": "هذه الخاصية غير متوفرة حاليا.",
"pleaseSelectTheRecipient": "المرجو اختيار المستلم",
"searchMemberIdHint": "يرجى إدخال رقم هوية العضو",
"thisFeatureIsCurrentlyUnavailable": "هذه الميزة غير متاحة حاليًا.",
"pleaseSelectTheRecipient": "يرجى اختيار المستلم.",
"searchMemberIdHint": "يرجى إدخال معرّف العضو",
"unclaimedRedEnvelopes": "الأظرفة الحمراء غير المطالب بها يتم استردادها خلال 24 ساعة.",
"sentARedEnvelope": "أرسل ظرف أحمر.",
"theRedEnvelopeHasExpired": "الظرف الأحمر منتهي الصلاحية",
@ -145,7 +145,7 @@
"systemAnnouncementTips1": "احذر الاحتيال:",
"systemAnnouncementTips": "تحقق من المعلومات عن طريق القنوات الرسمية فقط. لا تقم بتنزيل برامج من طرف ثالث، ولا تشارك البيانات الشخصية، ولا تحول المال بناءً على طلبات خارجية. بطاقات هوية الموظفين الرسمية هي فقط 10000 و10003 و10086. في حالة أي شك، توقف وقدم التقرير عبر",
"systemAnnouncement": "إعلان النظام",
"doNotClickUnfamiliarTips": "ماتضغطش على الروابط الغير معروفة، حيث قد تكشف معلوماتك الشخصية. متشاركش أبدا بطاقة الهوية أو معلومات البطاقة البنكية ديالك مع أي واحد.",
"doNotClickUnfamiliarTips": "لا تضغط على الروابط غير المألوفة، فقد تكشف معلوماتك الشخصية. لا تشارك رقم هويتك أو بيانات بطاقتك البنكية مع أي شخص.",
"atTag": "@تاغ",
"sayHi2": "قل مرحبا",
"roomBottomGreeting": "مرحبًا...",
@ -153,33 +153,33 @@
"reapply": "أعد التقديم",
"cancelRequest": "إلغاء الطلب",
"supporter": "مشجع",
"numberOfSign": "عدد العلامة: {1}",
"numberOfSign": "عدد مرات التوقيع: {1}",
"hostWeeklyRank": "تصنيف المضيف الأسبوعي",
"supporterWeeklyRank": رتيب الأسبوعي للمشجعين",
"supporterWeeklyRank": صنيف الداعمين الأسبوعي",
"memberList": "قائمة الأعضاء",
"treasureChest": "صندوق الكنز",
"applicationRecord": "سجل الطلبات",
"pending": "قيد الانتظار",
"appUpdateTip": "التطبيق لديه نسخة جديدة ({1})، هل تود الذهاب وتحميلها؟",
"reconcile": "يصالح",
"appUpdateTip": "يتوفر إصدار جديد من التطبيق ({1})، هل تريد تنزيله الآن؟",
"reconcile": "مصالحة",
"separated": "منفصل",
"areYouSureYouWantToSpend5": "{1} اعترف بالمشاعر ليك؛ إذا قبلتي، غادي توليو ثنائي.",
"areYouSureYouWantToSpend6": "{1} يريد العودة للتقارب معك. إذا قررت المصالحة، سيتم استعادة جميع بياناتك السابقة.",
"reconcileInvitationTips": "إذا الطرف الآخر رفض دعوة CP، غتترجع العملات ديالك للمحفظة ديالك.",
"areYouSureYouWantToSpend5": "{1} اعترف لك بمشاعره؛ إذا وافقت، ستصبحان ثنائيًا.",
"areYouSureYouWantToSpend6": "{1} يريد العودة إليك. إذا قررت المصالحة، فستتم استعادة جميع بياناتكما السابقة.",
"reconcileInvitationTips": "*إذا رفض الطرف الآخر دعوة CP، فستُعاد عملاتك إلى محفظتك.",
"reconcileInvitation": "دعوة للمصالحة",
"areYouSureYouWantToSpend4": "إرسال دعوة للمصالحة لهدا المستخدم؟",
"areYouSureYouWantToSpend4": "هل تريد إرسال دعوة مصالحة إلى هذا المستخدم؟",
"partWaysTips": "إذا قرر أحد الشريكين في العلاقة الانفصال، سيكون هناك فترة تهدئة لمدة 7 أيام. خلال هذه الفترة، يمكن للطرفين اختيار المصالحة، وسيتم استعادة كل البيانات عند المصالحة. إذا لم يتم اختيار المصالحة بنهاية فترة السبعة أيام، سيتم مسح بيانات الزوجين.",
"areYouSureYouWantToPartWaysWithYourCP": "هل أنت متأكد أنك تريد الانفصال عن شريكك؟",
"partWays": "افترقوا",
"partWays": "الانفصال",
"firstDay": "اليوم الأول:{1}",
"timeSpentTogether": "الوقت المشترك: {1} أيام",
"areYouSureYouWantToSpend3": "إذا الطرف الآخر رفض دعوة CP، غتترجع العملات ديالك للمحفظة ديالك.",
"areYouSureYouWantToSpend3": "*إذا رفض الطرف الآخر دعوة CP، فستُعاد عملاتك إلى محفظتك.",
"areYouSureYouWantToSpend2": "هل تريد إرسال دعوة CP لهذا المستخدم؟",
"areYouSureYouWantToSpend": "هل أنت متأكد أنك تريد أن تنفق",
"underReview": "قيد المراجعة",
"doYouWantToDeleteIt": "هل تريد حذفه؟",
"myPhoto": "صورتي",
"balanceNotEnough": "رصيد العملات الذهبية غير كافي. واش بغيت تزود الرصيد؟",
"balanceNotEnough": "رصيد العملات الذهبية غير كافٍ. هل تريد إعادة الشحن؟",
"skip": "تخطي {1}",
"chooseFromAblum": "اختر من الألبوم",
"information": "معلومات",
@ -187,16 +187,16 @@
"editProfile": "تعديل الملف الشخصي",
"sendTheCpRequest": "أرسل طلب CP",
"addCp": "أضف CP",
"numberOfMyCPs": "عدد نقاط السيطرة ديالي:({1}/{2})",
"numberOfMyCPs": "عدد علاقات CP الخاصة بي:({1}/{2})",
"relationShip": "علاقة",
"props": "الدعائم",
"couple2": "زوج",
"couple2": "ثنائي",
"dice": "نرد",
"operationsAreTooFrequent": "العمليات متكررة بشكل مفرط",
"luckNumber": "رقم الحظ",
"rps": "حجر ورقة مقص",
"couple": "زوج {1}:",
"noMatchedCP": "لا يوجد CP مطابق",
"noMatchedCP": "لا توجد علاقة CP مطابقة",
"adminInviteRechargeAgent": "ندعوك لتصبح وكيلا لإعادة الشحن.",
"ownerIncomeCoins": "دخل المالك: {1} عملة",
"allGames": "كل الألعاب",
@ -207,9 +207,9 @@
"greedyClass": "الفئة الجشعة",
"hotGames": "الألعاب الرائجة",
"sent": "أُرسل",
"termsOfServicePrivacyPolicyTips": متابعتك، أنت توافق على شروط الخدمة و سياسة الخصوصية",
"and": " اندر ",
"chatBox": "صندوق الشات",
"termsOfServicePrivacyPolicyTips": المتابعة، فإنك توافق على شروط الخدمة وسياسة الخصوصية",
"and": " و ",
"chatBox": "صندوق الدردشة",
"confirm": "تأكيد",
"cancel": "إلغاء",
"join": "انضم",
@ -217,23 +217,23 @@
"fans": "المعجبون",
"items": "عناصر",
"letGoToWatch": "هيا نذهب للمشاهدة!",
"launchedARocket": "أطلق صاروخ",
"launchedARocket": "أطلق صاروخًا",
"sendUserId": "أرسل معرف المستخدم:{1}",
"giveUpIdentity": "تخلى عن الهوية",
"leaveRoomIdentityTips": "هل أنت متأكد من أنك تريد التخلي عن هوية الغرفة؟",
"joinMemberTips2": "هل تريد تأكيد انضمامك إلى الغرفة كعضو؟",
"sureUnfollowThisRoom": "متأكد بغيتي تحيد المتابعة لهاذ الغرفة؟",
"sureUnfollowThisRoom": "هل أنت متأكد من إلغاء متابعة هذه الغرفة؟",
"settings": "الإعدادات",
"alreadyAnTourist": "أنت بالفعل سائح",
"deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟",
"account": "أكونتي",
"account": "الحساب",
"youHaventFollowed": "أنت لم تتابع أي غرفة",
"messageHasBeenRecalled": "تم استرجاع هذه الرسالة",
"propMessagePrompt": "موجه رسالة دعائية",
"deleteFromMyDevice": "احذف من جهازي",
"deleteOnAllDevices": "حذف على جميع الأجهزة",
"inputUserId": "أدخل معرف المستخدم",
"common": "شائع",
"common": "عام",
"useCoupontips": "هل أنت متأكد أنك تريد استخدام القسيمة؟",
"searchUserId": "ابحث عن معرف المستخدم",
"receiveSucc": "تم المطالبة بنجاح",
@ -242,22 +242,22 @@
"bio": "معلومات شخصية",
"delete": "حذف",
"sendCoupontips": "هل أنت متأكد أنك تريد إرسال هذه القسيمة لهذا المستخدم؟",
"recallThisMessage": "تذكر هذه الرسالة؟",
"recallThisMessage": "هل تريد استرجاع هذه الرسالة؟",
"copy": "نسخ",
"youDontHaveAnyCouponsYet": "ليس لديك أي كوبونات بعد",
"hobby": "هواية",
"recall": "استدعاء",
"accept": "اقبل",
"signedin": "تم التوقيع",
"following": "التالي",
"inviteYouToBecomeBD": "ندعوك لتصبح BD",
"recall": "استرجاع",
"accept": "قبول",
"signedin": "تم تسجيل الدخول",
"following": "يتابع",
"inviteYouToBecomeBD": "ندعوك للانضمام كـ BD.",
"confirmAcceptTheInvitation": "هل تريد تأكيد قبول الدعوة؟",
"confirmDeclineTheInvitation": "تأكيد رفض الدعوة؟",
"friends": "أصدقاء",
"language": "اللغة",
"feedback": "تعليق",
"feedback": "ملاحظات",
"coupon": "قسيمة",
"get": "احصل",
"get": "احصل على",
"couponRecord": "سجل استخدام القسيمة",
"inRoom": "في الغرفة",
"inRocket": "في الصاروخ",
@ -265,21 +265,21 @@
"searchCouponHint": "ابحث عن كوبون",
"search": "بحث",
"checkInSuccessful": "تسجيل الوصول ناجح",
"about": "حوالي",
"about": "حول",
"inviteYouToBecomeHost": "ندعوك لتصبح مضيفًا",
"receive": "استقبل",
"receive": "استلام",
"sginTips": "ستحصل على المكافأة في أول مرة تسجل دخولك فيها كل يوم. إذا قمت بقطع تسجيل دخولك، ستُحسب المكافأة من أول يوم تسجل فيه دخولك مرة أخرى.",
"aboutUs": "حولنا",
"theme": "موضوع",
"home": "المنزل",
"aboutUs": "من نحن",
"theme": "السمة",
"home": "الرئيسية",
"luck": "حظ",
"bDLeaderInviteYouToBecomeBDLeader": "ندعوك لتصبح قائد تطوير الأعمال",
"win2": "فوز {1}",
"level": "مستوى",
"wealthLevel": "مستوى الثروة",
"userLevel": "مستوى المستخدم",
"themeGoToUploadTips": "1.سيتم مراجعة التحميل خلال 24 ساعة بعد نجاحه. 2.\n سيتم إرجاع جميع العملات إذا فشلت المراجعة.",
"goToUpload": "اذهب لتحميل",
"themeGoToUploadTips": "1. ستتم مراجعة التحميل خلال 24 ساعة بعد نجاحه.\n2. ستُعاد جميع العملات إذا فشلت المراجعة.",
"goToUpload": "الانتقال للرفع",
"rechargeAgency": "وكالة الشحن",
"logout": "تسجيل الخروج",
"adminCenter": "مركز الإدارة",
@ -289,7 +289,7 @@
"fansList": "قائمة المعجبين",
"pleaseSelectTheTypeContent": "يرجى اختيار نوع المحتوى المخالف",
"me": "أنا",
"socialPrivilege": "امتياز اجتماعي",
"socialPrivilege": "امتيازات اجتماعية",
"spam": "بريد مزعج",
"inappropriateContent": "محتوى غير لائق",
"illegalInformation": "معلومات غير قانونية",
@ -297,11 +297,11 @@
"identity": "الهوية",
"adjust": "تعديل",
"roomReward": "مكافأة الغرفة",
"insufhcientGoldsGoToRecharge": "الذهب غير كافي، عاود الشحن دابا!",
"insufhcientGoldsGoToRecharge": "الرصيد غير كافٍ، يرجى إعادة الشحن الآن!",
"received": "تم الاستلام",
"goToRecharge": "اذهب لإعادة الشحن",
"warning": "تحذير",
"ownerSendTheRedEnvelope": "المالك أرسل عملات المكافأة",
"ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.",
"rewardCoins": "عملات المكافأة:{1} عملة",
"lastWeekProgress": "تقدم الأسبوع الماضي",
"currentProgress": "التقدم الحالي",
@ -313,7 +313,7 @@
"remainingNumberTips": "العدد المتبقي المتاح: ({1}/{2})",
"collectionTimeTips": "وقت الجمع:{1}({2}/{3})",
"sendARedEnvelope": "أرسل ظرفاً أحمر",
"sendRedPackConfirmTips": "هل أنت متأكد أنك تريد إرسال الحزمة الحمراء؟",
"sendRedPackConfirmTips": "هل أنت متأكد أنك تريد إرسال الظرف الأحمر؟",
"redEnvelopeSendingRecords": "سجلات إرسال الظرف الأحمر:",
"countdownMinutes": "دقائق العد التنازلي:",
"number2": "رقم:",
@ -326,12 +326,12 @@
"roomTools": "أدوات الغرفة:",
"entertainment": "الترفيه:",
"reportSucc": "تم الإبلاغ بنجاح",
"reportInputTips": "من فضلك وصف المشكل بأكبر قدر ممكن من التفاصيل باش نقدر نفهموه ونحلوه.",
"pornography": "الاباحية",
"reportInputTips": "يرجى وصف المشكلة بأكبر قدر ممكن من التفاصيل حتى نتمكن من فهمها وحلها.",
"pornography": "إباحية",
"screenshotTips": "لقطة شاشة (حتى 3)",
"roomEditing": "تحرير الغرفة",
"picture": "صورة",
"inputDesHint": "من فضلك وصف المشكلة بأكبر قدر ممكن من التفاصيل باش نقدر نفهموها ونحلها.",
"inputDesHint": "يرجى وصف المشكلة بأكبر قدر ممكن من التفاصيل حتى نتمكن من فهمها وحلها.",
"description": "الوصف:",
"roomNotice": "إشعار الغرفة",
"roomTheme": "موضوع الغرفة",
@ -343,25 +343,25 @@
"enterTheUserId": "أدخل معرف المستخدم",
"enterTheRoomId": "أدخل رقم الغرفة",
"theImageSizeCannotExceed": "حجم الصورة لا يمكن أن يتجاوز 4 ميغابايت",
"bdLeader": "زعيم بنك التنمية",
"bdLeader": "قائد BD",
"kickedOutOfRoom": "تم طرده من الغرفة",
"lockTheMic": "اقفل الميكروفون",
"openTheMic": "افتح الميكروفون",
"finish": "أنهى",
"lockTheMic": "قفل الميكروفون",
"openTheMic": "فتح الميكروفون",
"finish": "إنهاء",
"removeTheMic": "أزل الميكروفون",
"leavelTheMic": "اترك الميكروفون",
"unlockTheMic": "افتح الميكروفون",
"unlockTheMic": "إلغاء قفل الميكروفون",
"muteTheMic": "كتم الميكروفون",
"inviteToTheMicrophone": "دعوة للتحدث في الميكروفون",
"takeTheMic": "خذ الميكروفون",
"openUserProfleCard": "افتح بطاقة ملف المستخدم",
"crop": "محصول",
"crop": "قص",
"host": "مضيف",
"agent": "وكالة",
"ra": "را",
"bd": "بي دي",
"agent": "وكيل",
"ra": "RA",
"bd": "BD",
"unread": "غير مقروء",
"read": "اقرأ",
"read": "مقروء",
"image": "[صورة]",
"video": "[فيديو]",
"sound": "[صوت]",
@ -371,13 +371,13 @@
"giftCounter": "عداد الهدايا",
"wins": "يفوز",
"deleteAccount": "حذف الحساب",
"becomeAgent": "أصبح وكيلا",
"becomeAgent": "كن وكيلاً",
"male": "ذكر",
"setAccount": "تعيين الحساب",
"setAccount": "إعداد الحساب",
"female": "أنثى",
"album": "ألبوم",
"loadingFailedClickToRetry": "فشل التحميل، انقر لإعادة المحاولة",
"releaseToLoadMore": "حرر للتحميل المزيد",
"releaseToLoadMore": "حرر للتحميل",
"haveMyLimits": "---لدي حدودي.---",
"pullToLoadMore": "اسحب للتحميل المزيد",
"enterNickname": "أدخل الاسم المستعار",
@ -394,17 +394,17 @@
"saturday": "السبت {1}",
"sunday": "الأحد {1}",
"notifcation": "إشعار",
"inviteYouToBecomeAgent": "ندعوك لتصبح وكالة",
"inviteYouToBecomeAgent": "ندعوك لتصبح وكيلاً.",
"theVideoSizeCannotExceed": "لا يمكن أن يتجاوز حجم الفيديو 50 ميغابايت",
"fromLuckyGifts": "من الهدايا المحظوظة",
"fromLuckyGifts": "من هدايا الحظ/السحر",
"confirmSwitchMicModelTips": "هل تؤكد تغيير وضعية الجلوس؟",
"classicMic": "ميكروفون كلاسيكي {1}",
"number": "رقم",
"micTheme": "ثيم المايك",
"micTheme": "سمة الميكروفون",
"chat": "دردشة",
"rejected": "مرفوض",
"refuse": "رفض",
"boxContributeTips": "لقد تم الاستثمار بالفعل اليوم، رجاءً لا تستثمر مرة أخرى",
"boxContributeTips": "تم الاستثمار اليوم بالفعل، يرجى عدم الاستثمار مرة أخرى.",
"help": "مساعدة",
"approved": "معتمد",
"onlineUsers": "المستخدمون عبر الإنترنت ({1}/{2}):",
@ -418,14 +418,14 @@
"joinRequest": "طلب الانضمام",
"gameRules": "قوانين اللعبة:",
"gift": "هدية",
"charmGameRulesTips": "قم بتشغيل لوحة التحكم لعرض العملات الهدايا المستلمة لجميع المستخدمين على الميكروفون، 1 عملة = 1 نقطة (هدية محظوظة 1 عملة = 0.04 نقطة).",
"charm": "تعويذة هدية",
"charmGameRulesTips": "شغّل لوحة التحكم لعرض عملات الهدايا المستلمة لجميع المستخدمين على الميكروفون؛ 1 عملة = 1 نقطة (هدية الحظ: 1 عملة = 0.04 نقطة).",
"charm": "سحر الهدايا",
"chats": "الدردشات",
"myMusic": "موسيقاي",
"membershipFeeTips1": "من فضلك حدد رسوم الانضمام لغرفتك. يمكن للمستخدمين الانضمام إلى غرفتك من خلال دفع الرسوم.",
"membershipFeeTips2": "الذهب المطلوب للعضو في الغرفة. صاحب الغرفة سيحصل على 50٪ من الذهب.",
"membershipFee": "رسوم العضوية",
"touristsSendText": "السائح يرسل نص",
"touristsSendText": "إرسال النص للزوار",
"freePrice": "الرسوم: 0-10000",
"pleaseChatFfriendly": "رجاءً تحدث بطريقة ودية",
"kickPrevention": "الوقاية من الركلات",
@ -435,39 +435,39 @@
"stop": "توقف",
"add": "أضف",
"scrollToTheBottom": "قم بالتمرير إلى الأسفل",
"apple": "تفاحة",
"apple": "آبل",
"music": "موسيقى",
"free": "مجاناً",
"goToUpgrade": "اذهب إلى الترقية",
"free": "مجاني",
"goToUpgrade": "الانتقال للترقية",
"google": "جوجل",
"exclusiveEmojiWillBeReleasedAfterBecoming": "سيتم إصدار الرموز التعبيرية الحصرية بعد أن تصبح",
"preventBeingBlocked": "منع الحظر",
"enableRankIncognitoMode": "تمكين وضع الترتيب الخفي",
"avoidBeingKicked": "تجنب الركل",
"privileges": "{1} الامتيازات",
"andAboveUsers": "{1} والمستخدمون أعلاه",
"privileges": "امتيازات {1}",
"andAboveUsers": "مستخدمو {1} فما فوق",
"everyone": "الجميع",
"basicPermissions": "الأذونات الأساسية",
"mysteriousInvisibility": "الاختفاء الغامض",
"antiBlock": "مضاد للانسداد",
"antiBlock": "مضاد للحظر",
"privateChat": "دردشة خاصة",
"storeDiscount": "خصم المتجر {1}",
"membershipFreeChatSpeak": "دردشة وتحدث بدون عضوية",
"priorityRoomSorting": "فرز الغرف حسب الأولوية",
"userColoredID": "معرّف المستخدم الملون",
"setLoginPassword": "حدد كلمة مرور تسجيل الدخول",
"setLoginPassword": "تعيين كلمة مرور تسجيل الدخول",
"theTwoPasswordsDoNotMatch": "كلمتا المرور لا تتطابقتان.",
"resetLoginPasswordtTips2": "كلمة المرور يجب أن تكون بطول 8-16 حرف ويجب أن تكون مزيج من الأحرف الإنجليزية الكبيرة والصغيرة والأرقام (ليس فقط أرقام)",
"resetLoginPasswordtTips2": "يجب أن تتكون كلمة المرور من 8 إلى 16 حرفًا، وأن تجمع بين الأحرف الإنجليزية الكبيرة والصغيرة والأرقام (وليس الأرقام فقط).",
"confirmYourPassword": "أكد كلمة المرور الخاصة بك",
"enterYourNewPassword": "أدخل كلمة مرورك الجديدة",
"setYourPassword": "عيّن كلمة المرور ديالك",
"setYourPassword": "عيّن كلمة المرور الخاصة بك",
"enterYourOldPassword": "أدخل كلمة السر القديمة الخاصة بك",
"inputYourOldPassword": "أدخل كلمة المرور القديمة الخاصة بك",
"resetLoginPasswordtTips1": "سجل الدخول باستخدام معرف المستخدم أو معرف النمط. من الآمن أكثر تسجيل الدخول باستخدام حساب لايكي.",
"resetLoginPasswordtTips1": "سجّل الدخول باستخدام معرّف المستخدم أو المعرّف المميز. ويُعد تسجيل الدخول بحساب yumi أكثر أمانًا.",
"resetLoginPassword": "إعادة تعيين كلمة مرور تسجيل الدخول",
"localMusic": "الموسيقى المحلية",
"confirmSwitchMicThemeTips": "هل تؤكد تغيير نمط المقعد؟",
"follow2": "اتبع:{1}",
"follow2": "المتابَعون: {1}",
"fans2": "المعجبون:{1}",
"vistors2": "الزوار:{1}",
"personal2": "شخصي:",
@ -476,30 +476,30 @@
"enterRoomName": "أدخل اسم الغرفة",
"theMembershipFee": "رسوم العضوية",
"theModificationsMade": "التعديلات التي تمت هذه المرة لن يتم حفظها بعد الخروج.",
"touristsTakeToTheMic": "السياح ياخذو الميكروفون.",
"touristsTakeToTheMic": "صعود الزوار إلى الميكروفون",
"weekly": "أسبوعي",
"conntinue": "استمر",
"logIn": "تسجيل الدخول",
"sayHi": ُل مرحبا..",
"sayHi": ل مرحبًا..",
"password": "كلمة السر",
"enterAccount": "دخول الحساب",
"enterAccount": "أدخل الحساب",
"enterPassword": "أدخل كلمة المرور",
"roomName": "اسم الغرفة",
"enterRoomTips": "{1} ادخل الغرفة",
"enterRoomTips": "{1} دخل الغرفة",
"startVoiceParty": "ابدأ حفلة صوتية!",
"freeChatSpeak": "دردش بحرية وتحدث",
"roomMember": "عضو الغرفة",
"popular": "مشهور",
"popular": "الأكثر رواجًا",
"coinsReceived": "العملات المستلمة",
"hotRooms": "غرف حارة",
"hotRooms": "الغرف الرائجة",
"viewMore": "عرض المزيد",
"noData": "لا توجد بيانات",
"recommend": "يوصي",
"follow": "تابع",
"recommend": "موصى به",
"follow": "متابعة",
"monthly": "شهريًا",
"message": "رسالة",
"bdCenter": "مركز BD",
"history": "تاريخ",
"bdCenter": "مركز BD",
"history": "السجل",
"users": "المستخدمون",
"rooms": "غرف",
"days": "أيام",
@ -507,61 +507,61 @@
"unFollow": "إلغاء المتابعة",
"searchInputHint": "أدخل رقم الحساب / الغرفة",
"kickRoomTips": "لقد تم طردك من الغرفة",
"joinRoomTips": "غرفة متضامة!",
"roomSetting": "تجهيز الغرفة",
"joinRoomTips": "انضم إلى الغرفة!",
"roomSetting": "إعدادات الغرفة",
"roomDetails": "تفاصيل الغرفة",
"systemRoomTips": "كن مهذبا ومحترما. يُمنع منعاً باتاً أي محتوى إباحي أو مبتذل في yumi. بمجرد اكتشافه، سيتم حظر الحساب بشكل دائم. يرجى الالتزام بوعي بلوائح منصة yumi.",
"copiedToClipboard": "تم النسخ إلى الحافظة",
"recharge": "إعادة الشحن",
"agentCenter": "مركز الوكالة",
"report": "تقرير",
"report": "إبلاغ",
"done": "تمّ",
"improvementTasks": "مهام التحسين",
"followedYou": "تابعتك",
"followedYou": "قام بمتابعتك",
"followSucc": "تمت المتابعة بنجاح",
"edit": "تحرير",
"task": "مهمة",
"save": "احفظ",
"save": "حفظ",
"go": "اذهب",
"deleteAccount2": "حذف الحساب({1}s)",
"deleteAccount2": "حذف الحساب ({1}ث)",
"areYouSureYouWantToDeleteYourAccount": "هل أنت متأكد أنك تريد حذف حسابك؟",
"enterRoomConfirmTips": "هل أنت متأكد أنك تريد دخول الغرفة؟",
"dailyTasks": "المهام اليومية",
"nickName": "لقب",
"giftSpecialEffects": "هدية المؤثرات الخاصة",
"nickName": "الاسم المستعار",
"giftSpecialEffects": "تأثيرات الهدايا الخاصة",
"country": "دولة",
"basicFeatures": "الميزات الأساسية",
"floatingAnimationInGlobal": "الرسوم المتحركة العائمة في العالم",
"joinMemberTips": "إذا كنت زائرًا في الغرفة، لا يمكنك أخذ الميكروفون.",
"countryRegion": "البلد والمنطقة",
"gender": "جنس",
"likedYourComment": "أعجبتني طاقتك.",
"gender": "الجنس",
"likedYourComment": "أعجب بتعليقك.",
"deleteAccountTips2": "*إذا غيرت رأيك، يمكنك تسجيل الدخول مرة أخرى إلى حسابك الحالي خلال سبعة أيام، وسنقوم تلقائيًا باستعادة حسابك. إذا لم تتم عملية الاستعادة خلال سبعة أيام، فسيتم حذف الحساب نهائيًا",
"deleteAccountTips": "لديك صلاحيات إدارية كاملة على هذا الحساب. إذا كنت تنوي حذف الحساب، يُرجى الانتباه إلى المخاطر التالية المرتبطة بهذه العملية:\n\n1. بمجرد حذف الحساب بنجاح، لن تتمكن من تسجيل الدخول إليه. حذف الحساب إجراء نهائي.\n\n2. بعد حذف الحساب بنجاح، لن تتمكن من استعادة أي بيانات. سيتم حذف جميع المعلومات (بما في ذلك الغرف، والأصدقاء)، والعملة الافتراضية، والهدايا، والعناصر الافتراضية نهائيًا ولن يكون بالإمكان استعادتها.\n\n3. فترة السماح: إذا لم تقم باستعادة الحساب، فلن تتمكن من الوصول إلى صفحة الشراء، أو صفحة السحب، أو أي صفحات أخرى في التطبيق.\n\n4. خلال فترة السماح أو بعد حذف الحساب، ستشير صفحة الملف الشخصي إلى أنه قد تم حذفه. لحماية حسابك من البحث أو الوصول إليه من قِبل الآخرين، سيتم حذف معلوماتك الشخصية من الأنظمة المتعلقة بالوظائف اليومية. عندما يتعلق حذف الحساب بالأمن القومي، أو الإجراءات المدنية أو الجنائية، أو حماية الحقوق والمصالح المشروعة لأطراف ثالثة، يحتفظ المسؤول بحقه في رفض طلب حذف حساب المستخدم.\n\nإذا كنت متأكدًا من رغبتك في حذف جميع بياناتك الشخصية من حسابك الحالي، يُرجى النقر على \"حذف الحساب\".",
"accountDeletionNotice": "إشعار حذف الحساب:",
"entryVehicleAnimation2": "يمكن للمستخدمين الحاصلين على صلاحيات VlP4 أو أعلى استخدام هذه الوظيفة لتعطيل رسوميات المركبات المتحركة.",
"entryVehicleAnimation2": "يمكن للمستخدمين ذوي امتياز VIP4 أو أعلى استخدام هذه الميزة لتعطيل رسوم دخول المركبات المتحركة.",
"entryVehicleAnimation": "الرسوم المتحركة لدخول السيارة",
"man": "رجل",
"woman": "امرأة",
"all": "الكل",
"activity": "نشاط",
"knapsack": "حقيبه",
"areYouRureRoRecharge": "هل أنت متأكد من شحن ؟",
"knapsack": "الحقيبة",
"areYouRureRoRecharge": "هل أنت متأكد أنك تريد إعادة الشحن؟",
"clearMessage": "مسح رسائل الشاشة",
"pleaseSelectaItem": "يرجى اختيار البند",
"pleaseSelectaItem": "يرجى اختيار عنصر",
"birthday": "عيد ميلاد",
"pleaseEnterNickname": "يرجى إدخال لقب.",
"pleaseSelectYourCountry": "يرجى تحديد بلدك.",
"pleaseSelectYourGender": "يرجى تحديد جنسك.",
"dateOfBirth": "تاريخ الميلاد",
"mute": "صامت",
"mute": "كتم",
"exit": "خروج",
"send": "أرسل",
"goldList": "القائمة الذهبية",
"coins": "النقود المعدنية",
"coins": "عملات",
"lockTheRoom": "أغلق الغرفة",
"giftGivingSuccessful": "إن تقديم الهدايا كان ناجحًا.",
"hostCenter": "مركز الاستضافة",
"giftGivingSuccessful": "تم إرسال الهدية بنجاح.",
"hostCenter": "مركز المضيف",
"allOnMicrophone": "الجميع على الميكروفون",
"usersOnMicrophone": "المستخدمون على الميكروفون",
"mInimize": "احتفظ",
@ -572,33 +572,33 @@
"admin": "المشرف",
"member": "عضو",
"guest": "ضيف",
"submit": "تقديم",
"enter": "دخل",
"submit": "إرسال",
"enter": "دخول",
"unLockTheRoom": "افتح الغرفة",
"setRoomPassword": "تعيين كلمة مرور الغرفة",
"inputRoomPassword": "إدخال كلمة مرور الغرفة",
"operationSuccessful": "كانت العملية ناجحة.",
"adminByHomeowner": "يتم تعيين كمسؤول من قبل مالك المنزل.",
"memberByHomeowner": "يتم تعيين كأعضاء من قبل صاحب المنزل.",
"touristByHomeowner": "تم تعيين كسائح من قبل صاحب المنزل.",
"setUpAnIdentity": "أعد إعداد هوية.",
"allInTheRoom": "كل شيء في الغرفة.",
"operationSuccessful": "تمت العملية بنجاح.",
"adminByHomeowner": "تم تعيينه مشرفًا بواسطة مالك الغرفة.",
"memberByHomeowner": "تم تعيينه عضوًا بواسطة مالك الغرفة.",
"touristByHomeowner": "تم تعيينه زائرًا بواسطة مالك الغرفة.",
"setUpAnIdentity": "تعيين هوية",
"allInTheRoom": "الجميع داخل الغرفة",
"theAccountPasswordCannotBeEmpty": "لا يمكن أن يكون الحساب أو كلمة المرور خالية.",
"goldListort": "القائمة الذهبية",
"rechargeList": "قائمة إعادة الشحن",
"becomeHost": "قدّم لتصبح مضيفًاً",
"alreadyAnAdministrator": "مسؤول بالفعل.ً",
"alreadyAnMember": "أنت بالفعل عضو.ً",
"becomeHost": "التقدم لتصبح مضيفًا",
"alreadyAnAdministrator": "هو مشرف بالفعل.",
"alreadyAnMember": "هو عضو بالفعل.",
"touristsCannotSendMessages": "لا يستطيع السائحون إرسال الرسائل",
"touristsAreNotAllowedToGoOnTheMic": "لا يسمح للسياح بالدخول إلى الميكروفون",
"superFans": "المشجعين الخارقين:ً",
"touristsAreNotAllowedToGoOnTheMic": "لا يُسمح للزوار بالصعود إلى الميكروفون",
"superFans": "كبار المعجبين:",
"special": "خاص",
"custom": "مخصص",
"store": "دكان",
"store": "المتجر",
"viewFrame": "عرض الإطار",
"headdress": "إطارات",
"mountains": "مركبات",
"buy": "ابتاع",
"buy": "شراء",
"visitorList": "قائمة الزوار",
"spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة",
"howToUpgrade": "كيف يمكن الترقية؟",
@ -609,26 +609,26 @@
"obtain": "الحصول على",
"backTheRoom": "الغرفة الخلفية",
"toConsume": "الاستهلاك",
"profile": "بروفايل",
"profile": "الملف الشخصي",
"wallet": "محفظة",
"giftwall": يفت وول",
"giftwall": دار الهدايا",
"announcement": "إعلان",
"blockedList": "قائمة محظورة",
"blockedList": "قائمة الحظر",
"renewal": "تجديد",
"country2": "بلد:",
"sendTo": "أرسل إلى",
"credits": "الاعتمادات: {1}",
"successfullyUnloaded": "تم تفريغها بنجاح",
"unUse": "استخدام واحد",
"successfulWear": "ملابس ناجحة",
"confirmUnUseTips": "هل تؤكد على إزالته؟",
"successfullyUnloaded": "تمت الإزالة بنجاح",
"unUse": "إزالة الاستخدام",
"successfulWear": "تم الارتداء بنجاح",
"confirmUnUseTips": "هل تؤكد إزالة استخدامه؟",
"inUse": "قيد الاستخدام",
"confirmUseTips": "هل تريد التأكيد على استخدامه؟",
"pleaseUploadUserAvatar": "يرجى رفع صورة شخصية.",
"myItems": "أشيائي",
"myItems": "مقتنياتي",
"confirmBuyTips": "هل أنت متأكد من أنك تريد الشراء؟",
"purchaseIsSuccessful": "الشراء ناجح",
"purchase": "ابتاع",
"purchase": "شراء",
"invitesYouToTheMicrophone": "{1} يدعوك إلى الميكروفون",
"english": "الإنجليزية",
"chinese": "الصينية",
@ -636,6 +636,9 @@
"darkMode": "الوضع الداكن",
"lightMode": "الوضع الفاتح",
"systemDefault": "النظام الافتراضي",
"pleaseGetOnTheMicFirst": "من فضلك استخدم الميكروفون أولاً.",
"pleaseGetOnTheMicFirst": "يرجى الصعود إلى الميكروفون أولاً.",
"welcomeMessage": "مرحبًا بك في تطبيقنا، {name}!",
"operationFail": "فشلت العملية.",
"doYouWantToKeepTheDraft": "هل تريد الاحتفاظ بالمسودة؟",
"duration2": "المدة:{1}"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
"privaceyPolicy": "Gizlilik Politikası",
"tips": "İpuçları",
"searchNoDataTips": "Aramak istediğiniz oda veya kullanıcı kimliğini girin.",
"mine": "Benimki",
"mine": "Benim",
"party": "Parti",
"myRoom": "Odam",
"other": "Diğer",
@ -27,7 +27,7 @@
"blockedList2": "Engellenen Liste",
"roomTheme2": "Oda Teması",
"roomPassword": "Oda Şifresi",
"noHistoricalRecordsAvailable": "Mevcut tarihi kayıt yok.",
"noHistoricalRecordsAvailable": "Geçmiş kayıt bulunamadı.",
"inviteNewUsersToEarnCoins": "Yeni kullanıcıları davet ederek coin kazanın",
"crateMyRoom": "KENDİ ODANIZI OLUŞTURUN.",
"event": "Etkinlik",
@ -45,7 +45,7 @@
"trend": "Trend",
"like": "Beğen",
"more": "Daha Fazla",
"discard": "Vur",
"discard": "Vazgeç",
"catchFirstComment": "İlk Yorumu Yakala",
"reply": "Cevapla",
"posting": "Gönderiliyor",
@ -66,9 +66,9 @@
"systemAnnouncementTips1": "Sahtekarlığa karşı dikkat:",
"systemAnnouncementTips": "Bilgileri yalnızca resmi kanallar aracılığıyla doğrulayın. Hiçbir zaman üçüncü taraf yazılımı indirme, kişisel verileri paylaşma veya dış istekler üzerine para transferi yapmayın. Resmi personel kimlik numaraları yalnızca 10000, 10003 ve 10086'dır. Herhangi bir şüpheniz olursa, işlemi durdurun ve üzerinden bildirin",
"systemAnnouncement": "Sistem Duyurusu",
"doNotClickUnfamiliarTips": "Tanımadığınız bağlantılara tıklayın, çünkü bunlar kişisel bilgilerinizi ifşa edebilir. Kimlik numaranızı veya banka kartı detaylarınızı asla kimseyle paylaşmayın.",
"doNotClickUnfamiliarTips": "Tanımadığınız bağlantılara tıklamayın; bunlar kişisel bilgilerinizi açığa çıkarabilir. Kimlik numaranızı veya banka kartı bilgilerinizi asla kimseyle paylaşmayın.",
"atTag": "@Etiket",
"sayHi2": "Merhaba De",
"sayHi2": "Merhaba",
"roomBottomGreeting": "Merhaba...",
"canSendMsgTips": "Özel mesaj göndermek için her iki tarafın da birbirini takip etmesi gerekir.",
"msgSendRedEnvelopeTips": "*Kırmızı zarflar üzerinde %10 hizmet ücreti kesilecektir ve alıcılar yalnızca kırmızı zarfın değerinin %90'ını alacaktır. Göndericinin servet seviyesi 10. Seviyeden yüksek olmalıdır.",
@ -83,7 +83,7 @@
"memberList": "Üye Listesi",
"treasureChest": "Hazine Sandığı",
"applicationRecord": "Başvuru Kaydı",
"appUpdateTip": "Uygulamanın yeni bir sürümü var ({1}), lütfen indirin mi?",
"appUpdateTip": "Uygulamanın yeni bir sürümü ({1}) var, şimdi indirmek ister misiniz?",
"ownerIncomeCoins": "Sahibin Geliri:{1} jetton",
"game": "Oyun",
"skip2": "Atla",
@ -104,7 +104,7 @@
"termsOfServicePrivacyPolicyTips": "Devam ederek Hizmet Şartları'nı ve Gizlilik Politikası'nı kabul ediyorsunuz",
"and": " ve ",
"pleaseSelectTheTypeContent": "Lütfen ihlalci içeriğin türünü seçin.",
"illegalInformation": "Yasaksız Bilgi",
"illegalInformation": "Yasa Dışı Bilgi",
"inappropriateContent": "Uygunsuz İçerik",
"personalAttack": "Kişisel Saldırı",
"confirm": "Onayla",
@ -122,14 +122,14 @@
"lastWeekProgress": "Geçen Haftanın İlerlemesi",
"redEnvelopeTips2": "*Kırmızı zarf zaman sınırı içinde talep edilmezse, kalan jettonlar gönderen kullanıcıya iade edilecektir.",
"goToRecharge": "Yüklemeye Git",
"deleteAccount2": " Hesabı Sil({1}s)",
"deleteAccount2": " Hesabı Sil ({1} sn)",
"areYouSureYouWantToDeleteYourAccount": " Hesabınızı silmek istediğinizden emin misiniz?",
"insufhcientGoldsGoToRecharge": "Altın yetersiz, hemen yükle!",
"coins2": "{1}Jetton",
"coins2": "{1} Jetton",
"remainingNumberTips": "Kalan Kullanılabilir Sayı:({1}/{2})",
"collectionTimeTips": "Toplama Zamanı:{1}({2}/{3})",
"sendARedEnvelope": "Kırmızı Zarf Gönder",
"sendRedPackConfirmTips": "Kırmızı paket göndermek istediğinizden emin misiniz?",
"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.",
@ -139,7 +139,7 @@
"redEnvelopeTips1": "Jettonlar:",
"roomTools": "Oda Araçları:",
"entertainment": "Eğlence:",
"reportSucc": "Bildirim başarılı",
"reportSucc": "Bildirim başarıyla gönderildi",
"pornography": "Pornografi",
"reportInputTips": "Sorunu anlayabilmemiz ve çözebilmemiz için lütfen sorunu mümkün olduğunca detaylı anlatın.",
"cancel": "İptal",
@ -173,7 +173,7 @@
"youHaventFollowed": "Hiç oda takip etmediniz",
"deleteFromMyDevice": "Cihazımdan Sil",
"deleteOnAllDevices": "Tüm Cihazlarda Sil",
"messageHasBeenRecalled": "Bu mesaj geri çırıldı",
"messageHasBeenRecalled": "Bu mesaj geri çekildi",
"recallThisMessage": "Bu mesajı geri çağırmak ister misiniz?",
"language": "Dil",
"feedback": "Geri Bildirim",
@ -188,7 +188,7 @@
"logout": ıkış Yap",
"luck": "Şans",
"level": "Seviye",
"themeGoToUploadTips": "1.Yükleme başarılı olduktan sonra 24 saat içinde inceleme yapılacaktır.\n2.Inceleme başarısız olursa tüm jettonlar iade edilecektir.",
"themeGoToUploadTips": "1. Yükleme başarılı olduktan sonra 24 saat içinde inceleme yapılacaktır.\n2. İnceleme başarısız olursa tüm jettonlar iade edilecektir.",
"home": "Anasayfa",
"explore": "Keşfet",
"me": "Ben",
@ -210,14 +210,14 @@
"areYouSureYouWantToSpend2": "bu kullanıcıya CP daveti göndermek için?",
"underReview": "İnceleniyor",
"doYouWantToDeleteIt": "Silmek ister misiniz?",
"chooseFromAblum": "Albümdan Seç",
"chooseFromAblum": "Albümden Seç",
"spaceBackground": "Mekan Arka Planı",
"editProfile": "Profili Düzenle",
"sendTheCpRequest": "CP İsteğini Gönder",
"addCp": "CP Ekle",
"partWays": "Yollarını Ayırmak",
"partWays": "Yolları Ayır",
"reconcile": "Uzlaşmak",
"separated": "Ayırılmış",
"separated": "Ayrıldı",
"areYouSureYouWantToSpend5": "{1} size hislerini ifade etti; kabul ederseniz çift olacaksınız.",
"areYouSureYouWantToSpend6": "{1} sizinle tekrar bir araya gelmek istiyor. Uzlaşmaya karar verirseniz, tüm önceki verileriniz geri yüklenecektir.",
"reconcileInvitationTips": "*Diğer taraf CP davetini reddederse, jettonlarınız cüzdanınıza iade edilecektir.",
@ -231,7 +231,7 @@
"props": "Özellikler",
"win": "Kazanan",
"dice": "Zar",
"rps": "Kağıt-Kaçak-Makas",
"rps": "Taş-Kağıt-Makas",
"operationFail": "İşlem başarısız oldu.",
"likedYourComment": "Yorumunuzu beğendi.",
"doYouWantToKeepTheDraft": "Taslağı saklamak ister misiniz?",
@ -262,19 +262,19 @@
"roomAnnouncement": "Oda Duyurusu",
"help": "Yardım",
"rejected": "Reddedildi",
"boxContributeTips": "Bugün zaten yatırım yapıldı, lütfen tekrar yatırım yapmayın",
"boxContributeTips": "Bugün zaten yatırım yapıldı, lütfen tekrar yatırım yapmayın.",
"bd": "BD",
"coupon": "Kupon",
"search": "Ara",
"get": "Al",
"inRocket": "Rokette",
"roomRocketHelpTips": "1. Odada hediye göndermek roket enerjisini artırır. *1 altın jetton hediyesi = 1 roket enerji puanı; şanslı hediyeler roket enerjisini hediyenin altın jetton değerinin %4'ü kadar artırır.\n2. Roket enerjisi tamamen dolduğunda, oda roketi fırlatabilir. Fırlatmadan sonra ödüller otomatik olarak dağıtılacaktır.\n3. Farklı roket seviyeleri farklı ödüller sunar.\n4. Roket fırlatıldığında, odadaki tüm kullanıcılar roket ödülünü talep edebilir.5. Roket enerjisi her gün 00:00'da sıfırlanır.",
"roomRocketHelpTips": "1. Odada hediye göndermek roket enerjisini artırır. *1 altın jetton hediyesi = 1 roket enerji puanı; şanslı hediyeler roket enerjisini hediyenin altın jetton değerinin %4'ü kadar artırır.\n2. Roket enerjisi tamamen dolduğunda, oda roketi fırlatabilir. Fırlatmadan sonra ödüller otomatik olarak dağıtılacaktır.\n3. Farklı roket seviyeleri farklı ödüller sunar.\n4. Roket fırlatıldığında, odadaki tüm kullanıcılar roket ödülünü talep edebilir.\n5. Roket enerjisi her gün 00:00'da sıfırlanır.",
"couponRecord": "Kupon Kullanım Kaydı",
"inRoom": "Odada",
"searchCouponHint": "Kupon Ara",
"giftCounter": "Hediye Sayacı",
"bDLeaderInviteYouToBecomeBDLeader": "Sizi BD Lideri yapmaya davet ediyoruz",
"wins": "kazanmalar",
"wins": "kazandı",
"inviteYouToBecomeHost": "Sizi sunucu yapmaya davet ediyoruz.",
"friends": "Arkadaşlar",
"deleteConversationTips": "Bu kullanıcıyla olan sohbet geçmişini silmek istediğinizden emin misiniz?",
@ -285,7 +285,7 @@
"checkInSuccessful": "Giriş başarılı",
"sginTips": "Her gün ilk defa giriş yaptığınızda ödül alacaksınız. Girişi keserseniz, tekrar giriş yaptığınızda ödül ilk günden itibaren hesaplanacaktır.",
"popular": "Popüler",
"recommend": "Öner",
"recommend": "Önerilen",
"follow": "Takip Et",
"history": "Tarihçe",
"hotRooms": "Popüler Odalar",
@ -347,7 +347,7 @@
"localMusic": "Yerel Müzik",
"setLoginPassword": "Giriş Şifresi Ayarlayın",
"confirmSwitchMicThemeTips": "Koltuk stili değiştirmeyi onaylıyor musunuz?",
"micTheme": "Mikro Tema",
"micTheme": "Mikrofon Teması",
"classicMic": "Klasik {1} Mikro",
"yesterday": "Dün {1}",
"monday": "Pazartesi {1}",
@ -375,7 +375,7 @@
"warning": "Uyarı",
"screenshotTips": "Ekran Görüntüsü (En Fazla 3)",
"roomNotice": "Oda Bildirimi",
"roomTheme": "Oda Tema",
"roomTheme": "Oda Teması",
"description": "Açıklama:",
"inputDesHint": "Sorunu anlayabilmemiz ve çözebilmemiz için lütfen sorunu mümkün olduğunca detaylı anlatın.",
"roomProfilePicture": "Oda Profil Fotoğrafı",
@ -408,7 +408,7 @@
"joinRoomTips": "odaya katıldı !",
"roomSetting": "Oda Ayarları",
"roomDetails": "Oda Detayları",
"systemRoomTips": "Neysevi ve saygılı olun. yumi'de herhangi bir pornografik veya uygunsuz içerik kesinlikle yasaktır. Keşfedilirse, hesap kalıcı olarak engellenecektir. Lütfen yumi platformunun düzenlemelerini bilinçli olarak takip edin.",
"systemRoomTips": "Nezaketli ve saygılı olun. yumi'de pornografik veya uygunsuz içerik kesinlikle yasaktır. Tespit edilmesi halinde hesap kalıcı olarak engellenecektir. Lütfen yumi platform kurallarına bilinçli şekilde uyun.",
"copiedToClipboard": "Panoya kopyalandı",
"recharge": "Yükle",
"receivedFromALuckyGift": "Şanslı/sihirli bir hediyeden alındı.",
@ -423,7 +423,7 @@
"task": "Görev",
"importantReminder": "Önemli Hatırlatma",
"entryVehicleAnimation": "Giriş Aracı Animasyonu",
"floatingAnimationInGlobal": "Küreselde Süzme Animasyon",
"floatingAnimationInGlobal": "Genel Ekranda Yüzen Animasyon",
"entryVehicleAnimation2": "VIP4 veya daha yüksek haklara sahip kullanıcılar araç animasyonlarını devre dışı bırakmak için fonksiyonu kullanabilir.",
"dailyTasks": "Günlük Görevler",
"enterRoomConfirmTips": "Odaya girmek istediğinizden emin misiniz?",
@ -431,7 +431,7 @@
"goldListort": "Altın Listesi",
"rechargeList": "Yükleme Listesi",
"edit": "Düzenle",
"swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*İpucu: Süzme ekran alanına sola kaydırarak hızlıca kapatın.",
"swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*İpucu: Hızlıca kapatmak için yüzen ekran alanında sola kaydırın.",
"enterThisVoiceChatRoom": "Bu sesli sohbet odasına girmek ister misiniz?",
"go": "Git",
"done": "Bitti",
@ -453,14 +453,14 @@
"userLevelXPBoost": "Kullanıcı Seviyesi XP Artışı({1} XP)",
"google": "Google",
"mysteriousInvisibility": "Gizemli görünmezlik",
"antiBlock": "Tıkanmayı Önleyici",
"antiBlock": "Engellenme Koruması",
"privateChat": "Özel Sohbet",
"everyone": "Herkes",
"goToUpgrade": "Yükseltmeye git",
"exclusiveEmojiWillBeReleasedAfterBecoming": "Özel emoji olunduktan sonra yayınlanacak",
"preventBeingBlocked": "Engellenmekten Kaçının",
"enableRankIncognitoMode": "Rütbe Gizli Modunu Etkinleştir",
"avoidBeingKicked": "Tekme Yemekten Kaçının",
"avoidBeingKicked": "Odadan Atılmayı Önleme",
"privileges": "{1} Ayrıcalıklar",
"andAboveUsers": "{1} ve üzeri kullanıcılar",
"basicPermissions": "Temel izinler",
@ -481,7 +481,7 @@
"exit": ıkış",
"pleaseSelectaItem": "Lütfen bir öğe seçin",
"areYouRureRoRecharge": "Yüklemek istediğinizden emin misiniz?",
"mInimize": "Tutmak",
"mInimize": "Küçült",
"shop": "Mağaza",
"expirationTime": "Son kullanma süresi",
"roomOwner": "Oda Sahibi",
@ -495,13 +495,13 @@
"takeTheMic": "Mikroyu Al",
"openTheMic": "Mikroyu Aç",
"muteTheMic": "Mikronun Sesini Kapat",
"unlockTheMic": "Mikroyu Kilidini Aç",
"unlockTheMic": "Mikrofon Kilidini Aç",
"leavelTheMic": "Mikroyu Bırak",
"lockTheMic": "Mikroyu Kilitle",
"removeTheMic": "Mikroyu Kaldır",
"inviteToTheMicrophone": "Mikroya Davet Et",
"openUserProfleCard": "Kullanıcı Profili Kartını Aç",
"obtain": "elde etmek",
"obtain": "Al",
"win2": "{1} Kazan",
"backTheRoom": "Odaya Dön",
"toConsume": "Tüketmek İçin",
@ -519,10 +519,10 @@
"submit": "Gönder",
"membershipFee": "Üyelik Ücreti",
"membershipFeeTips1": "Lütfen odanız için üyelik ücretini ayarlayın. Kullanıcılar ücreti ödeyerek odanıza katılabilir.",
"membershipFeeTips2": "Kullanıcı'nın oda üyesi olması için gerekli altınlar.Oda sahibi altınların %50'sini alacaktır.",
"membershipFeeTips2": "Kullanıcının oda üyesi olması için gereken altın miktarıdır. Oda sahibi altınların %50'sini alacaktır.",
"freePrice": "Ücret:0-10000",
"touristsSendText": "Turist metin gönderir",
"touristsTakeToTheMic": "Turist mikro alır",
"touristsSendText": "Ziyaretçi metin gönderebilir",
"touristsTakeToTheMic": "Ziyaretçi mikrofona geçebilir",
"theMembershipFee": "Üyelik Ücreti",
"theModificationsMade": "Bu sefer yapılan değişiklikler çıkıştan sonra kaydedilmeyecektir",
"viewFrame": "Çerçeveyi Gör",
@ -556,7 +556,7 @@
"saySomething": "Bir şey söyle...",
"sayHi": "Merhaba..",
"pleaseChatFfriendly": "Lütfen arkadaşça sohbet edin",
"unLockTheRoom": "Odayı Kilidini Aç",
"unLockTheRoom": "Odanın Kilidini Aç",
"operationSuccessful": "İşlem başarılı oldu.",
"adminByHomeowner": "ev sahibi tarafından yönetici olarak atandı.",
"memberByHomeowner": "ev sahibi tarafından üye olarak atandı.",
@ -568,7 +568,7 @@
"knapsack": "Sırt Çantası",
"bdLeader": "BD Lideri",
"picture": "Resim",
"claim": "İddia",
"claim": "Talep Et",
"taskNameRoomNewMember": "Yeni Oda Üyeleri",
"taskNameRoomOwnerSendRedPacket": "Oda Sahibi bir kırmızı paket gönderiyor",
"taskNameRoomOwnerSendGiftUser": "Oda Sahibi Hediye Gönderir",
@ -585,7 +585,7 @@
"taskNameRoomOwnerMicTime": "Oda Sahibi odada mikrofona geçer",
"taskNamePersonalActiveInRoom": "Başkalarının Odalarında Aktif Ol",
"taskNamePersonalGameConsume": "Oyun Harcaması",
"taskNamePersonalMicInRoom": "Mikrofonu Aç",
"taskNamePersonalMicInRoom": "Mikrofona Çık",
"complete": "Tamamla",
"shareTo": "Paylaş",
"faceBook": "Facebook",
@ -596,7 +596,7 @@
"dailyCoinBonanzaRules": "Günlük Jeton Çılgınlığı Kuralları",
"roomOwnerTasks": "Oda Sahibi Görevleri",
"personalTasks": "Kişisel Görevler",
"noPromptsToday": "Bugün hiçbir istem yok.",
"noPromptsToday": "Bugün görev yok.",
"getPaidToRefer": "Tavsiye Ederek Para Kazanın",
"theImageSizeCannotExceed": "Görsel boyutu 4M'yi geçemez",
"activity": "Etkinlik",
@ -630,7 +630,7 @@
"confirmUseTips": "Kullanmak için onaylıyor musunuz?",
"pleaseUploadUserAvatar": "Lütfen bir profil fotoğrafı yükleyin.",
"joinMemberTips": "Eğer odada turist iseniz, mikroyu alamazsınız.",
"giftGivingSuccessful": "Hediye verme başarılı.",
"giftGivingSuccessful": "Hediye başarıyla gönderildi.",
"theAccountPasswordCannotBeEmpty": "Hesap veya şifre boş olamaz.",
"invitesYouToTheMicrophone": "{1} seni mikroya davet ediyor",
"english": "İngilizce",

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:ui' as ui;
@ -32,6 +34,7 @@ import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/modules/gift/gift_tab_page.dart';
@ -47,16 +50,18 @@ class _GiftPageTabItem {
}
class GiftPage extends StatefulWidget {
SocialChatUserProfile? toUser;
final SocialChatUserProfile? toUser;
GiftPage({super.key, this.toUser});
const GiftPage({super.key, this.toUser});
@override
_GiftPageState createState() => _GiftPageState();
State<GiftPage> createState() => _GiftPageState();
}
class _GiftPageState extends State<GiftPage>
with SingleTickerProviderStateMixin {
class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
static const Duration _comboFeedbackDuration = Duration(seconds: 3);
static const Duration _comboSendBatchWindow = Duration(milliseconds: 200);
static const List<String> _preferredGiftTabOrder = <String>[
"ALL",
"ACTIVITY",
@ -97,6 +102,12 @@ class _GiftPageState extends State<GiftPage>
int giftType = 0;
Debouncer debouncer = Debouncer();
late final AnimationController _comboFeedbackController;
final ListQueue<_PendingGiftSendBatch> _comboSendBatchQueue =
ListQueue<_PendingGiftSendBatch>();
Timer? _comboSendBatchTimer;
bool _isComboSendBatchInFlight = false;
bool _showComboFeedback = false;
void _giftFxLog(String message) {
debugPrint('[GiftFX][Send] $message');
@ -200,6 +211,16 @@ class _GiftPageState extends State<GiftPage>
@override
void initState() {
super.initState();
_comboFeedbackController = AnimationController(
vsync: this,
duration: _comboFeedbackDuration,
)..addStatusListener((status) {
if (status == AnimationStatus.completed && mounted) {
setState(() {
_showComboFeedback = false;
});
}
});
rtcProvider = Provider.of<RtcProvider>(context, listen: false);
Provider.of<SCAppGeneralManager>(context, listen: false).giftList();
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
@ -224,8 +245,13 @@ class _GiftPageState extends State<GiftPage>
@override
void dispose() {
_comboSendBatchTimer?.cancel();
if (_comboSendBatchQueue.isNotEmpty) {
unawaited(_flushAllPendingComboGiftSends());
}
_tabController?.removeListener(_handleTabChanged);
_tabController?.dispose();
_comboFeedbackController.dispose();
super.dispose();
}
@ -781,29 +807,7 @@ class _GiftPageState extends State<GiftPage>
size: 20.w,
),
SizedBox(width: 5.w),
GestureDetector(
onTap: () {
giveGifts();
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.w,
horizontal: 20.w,
),
decoration: BoxDecoration(
color:
SocialChatTheme.primaryLight,
borderRadius:
BorderRadius.circular(5),
),
child: text(
SCAppLocalizations.of(
context,
)!.send,
fontSize: 14.sp,
),
),
),
_buildSendButton(),
],
),
),
@ -1068,8 +1072,7 @@ class _GiftPageState extends State<GiftPage>
}
}
///
void giveGifts() async {
_GiftSendRequest? _buildGiftSendRequest() {
List<String> acceptUserIds = [];
List<MicRes> acceptUsers = [];
@ -1102,7 +1105,7 @@ class _GiftPageState extends State<GiftPage>
'giveType=$giveType',
);
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient);
return;
return null;
}
final selectedGift = checkedGift;
if (selectedGift == null) {
@ -1111,41 +1114,18 @@ class _GiftPageState extends State<GiftPage>
'acceptUserIds=${acceptUserIds.join(",")} '
'giveType=$giveType',
);
return;
return null;
}
final selectedNumber = number;
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
final roomAccount =
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final isLuckyGiftRequest = _usesLuckyGiftEndpoint(selectedGift);
final requestName = isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
final senderId = AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
final senderName =
AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "";
final stopwatch = Stopwatch()..start();
_giftFxLog(
'tap send start request=$requestName '
'senderId=$senderId '
'senderName=$senderName '
'giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftTab=${selectedGift.giftTab} '
'special=${selectedGift.special} '
'standardId=${selectedGift.standardId} '
'giftCandy=${selectedGift.giftCandy} '
'number=$selectedNumber '
'acceptCount=${acceptUserIds.length} '
'acceptUserIds=${acceptUserIds.join(",")} '
'roomId=$roomId '
'roomAccount=$roomAccount '
'giveType=$giveType '
'giftType=$giftType',
);
if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) {
const configError =
'Gift configuration unavailable, please try another gift.';
final requestName = isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
_giftFxLog(
'$requestName skipped giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
@ -1154,84 +1134,248 @@ class _GiftPageState extends State<GiftPage>
'reason=invalid_standard_id',
);
SCTts.show(configError);
return null;
}
return _GiftSendRequest(
acceptUserIds: acceptUserIds,
acceptUsers: acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
roomId: roomId,
roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest,
);
}
///
void giveGifts() async {
final request = _buildGiftSendRequest();
if (request == null) {
return;
}
_activateComboFeedback(
gift: request.gift,
quantity: request.quantity,
acceptUserIds: request.acceptUserIds,
);
if (_supportsComboRequestBatching(request.gift)) {
_enqueueComboGiftSendRequest(request);
return;
}
await _performGiftSend(request, trigger: 'direct');
}
bool _supportsComboRequestBatching(SocialChatGiftRes gift) {
return _supportsComboFeedback(gift);
}
void _enqueueComboGiftSendRequest(_GiftSendRequest request) {
final now = DateTime.now();
_PendingGiftSendBatch? existingBatch;
for (final batch in _comboSendBatchQueue) {
if (batch.batchKey == request.batchKey) {
existingBatch = batch;
break;
}
}
if (existingBatch != null) {
existingBatch.quantity += request.quantity;
existingBatch.readyAt = now.add(_comboSendBatchWindow);
_giftFxLog(
'aggregate combo send update '
'batchKey=${existingBatch.batchKey} '
'giftId=${request.gift.id} '
'quantity=${existingBatch.quantity} '
'acceptUserIds=${request.acceptUserIds.join(",")}',
);
} else {
final batch = _PendingGiftSendBatch.fromRequest(
request,
readyAt: now.add(_comboSendBatchWindow),
);
_comboSendBatchQueue.add(batch);
_giftFxLog(
'aggregate combo send enqueue '
'batchKey=${batch.batchKey} '
'giftId=${request.gift.id} '
'quantity=${batch.quantity} '
'acceptUserIds=${request.acceptUserIds.join(",")}',
);
}
_scheduleNextComboGiftSendFlush();
}
void _scheduleNextComboGiftSendFlush() {
_comboSendBatchTimer?.cancel();
_comboSendBatchTimer = null;
if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) {
return;
}
final headBatch = _comboSendBatchQueue.first;
final delay = headBatch.readyAt.difference(DateTime.now());
if (delay <= Duration.zero) {
unawaited(_flushNextComboGiftSendBatch());
return;
}
_comboSendBatchTimer = Timer(delay, () {
unawaited(_flushNextComboGiftSendBatch());
});
}
Future<void> _flushNextComboGiftSendBatch() async {
_comboSendBatchTimer?.cancel();
_comboSendBatchTimer = null;
if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) {
return;
}
final headBatch = _comboSendBatchQueue.first;
if (headBatch.readyAt.isAfter(DateTime.now())) {
_scheduleNextComboGiftSendFlush();
return;
}
_comboSendBatchQueue.removeFirst();
_isComboSendBatchInFlight = true;
try {
await _performGiftSend(headBatch.toRequest(), trigger: 'batched');
} finally {
_isComboSendBatchInFlight = false;
_scheduleNextComboGiftSendFlush();
}
}
Future<void> _flushAllPendingComboGiftSends() async {
_comboSendBatchTimer?.cancel();
_comboSendBatchTimer = null;
while (_comboSendBatchQueue.isNotEmpty) {
if (_isComboSendBatchInFlight) {
await Future<void>.delayed(const Duration(milliseconds: 50));
continue;
}
_comboSendBatchQueue.first.readyAt = DateTime.now();
await _flushNextComboGiftSendBatch();
}
}
Future<void> _performGiftSend(
_GiftSendRequest request, {
required String trigger,
}) async {
final requestName = request.requestName;
final profileManager =
navigatorKey.currentState == null
? null
: Provider.of<SocialChatUserProfileManager>(
navigatorKey.currentState!.context,
listen: false,
);
final senderId = AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
final senderName =
AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "";
final stopwatch = Stopwatch()..start();
_giftFxLog(
'send start trigger=$trigger request=$requestName '
'senderId=$senderId '
'senderName=$senderName '
'giftId=${request.gift.id} '
'giftName=${request.gift.giftName} '
'giftTab=${request.gift.giftTab} '
'special=${request.gift.special} '
'standardId=${request.gift.standardId} '
'giftCandy=${request.gift.giftCandy} '
'number=${request.quantity} '
'acceptCount=${request.acceptUserIds.length} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'roomId=${request.roomId} '
'roomAccount=${request.roomAccount} '
'giveType=$giveType '
'giftType=$giftType',
);
try {
final repository = SCChatRoomRepository();
_giftFxLog(
'calling repository.$requestName '
'giftId=${selectedGift.id} '
'roomId=$roomId '
'acceptUserIds=${acceptUserIds.join(",")} '
'quantity=$selectedNumber',
'trigger=$trigger '
'giftId=${request.gift.id} '
'roomId=${request.roomId} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'quantity=${request.quantity}',
);
final result =
isLuckyGiftRequest
request.isLuckyGiftRequest
? await repository.giveLuckyGift(
acceptUserIds,
selectedGift.id ?? "",
selectedNumber,
request.acceptUserIds,
request.gift.id ?? "",
request.quantity,
false,
roomId: roomId,
roomId: request.roomId,
)
: await repository.giveGift(
acceptUserIds,
selectedGift.id ?? "",
selectedNumber,
request.acceptUserIds,
request.gift.id ?? "",
request.quantity,
false,
roomId: roomId,
roomId: request.roomId,
);
_giftFxLog(
'$requestName success giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftSourceUrl=${selectedGift.giftSourceUrl} '
'special=${selectedGift.special} '
'giftTab=${selectedGift.giftTab} '
'standardId=${selectedGift.standardId} '
'number=$selectedNumber '
'acceptUserIds=${acceptUserIds.join(",")} '
'roomId=$roomId '
'$requestName success trigger=$trigger '
'giftId=${request.gift.id} '
'giftName=${request.gift.giftName} '
'giftSourceUrl=${request.gift.giftSourceUrl} '
'special=${request.gift.special} '
'giftTab=${request.gift.giftTab} '
'standardId=${request.gift.standardId} '
'number=${request.quantity} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'roomId=${request.roomId} '
'balance=$result '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
// SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful);
if (isLuckyGiftRequest) {
if (request.isLuckyGiftRequest) {
_showLocalLuckyGiftFeedback(
acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
);
await sendLuckGiftAnimOtherMsg(
acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
);
} else {
sendGiftMsg(acceptUsers, gift: selectedGift, quantity: selectedNumber);
}
if (mounted) {
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).updateBalance(result);
sendGiftMsg(
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
);
}
profileManager?.updateBalance(result);
} catch (e) {
final errorMessage = _resolveGiftSendErrorMessage(e);
final errorDetails = _describeGiftSendError(e);
_giftFxLog(
'$requestName failed giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftTab=${selectedGift.giftTab} '
'standardId=${selectedGift.standardId} '
'$requestName failed trigger=$trigger '
'giftId=${request.gift.id} '
'giftName=${request.gift.giftName} '
'giftTab=${request.gift.giftTab} '
'standardId=${request.gift.standardId} '
'error=$e '
'resolvedError=$errorMessage '
'details={$errorDetails} '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
if (mounted) {
SCTts.show(errorMessage);
}
SCTts.show(errorMessage);
}
}
@ -1347,6 +1491,39 @@ class _GiftPageState extends State<GiftPage>
}
}
void _activateComboFeedback({
required SocialChatGiftRes gift,
required int quantity,
required List<String> acceptUserIds,
}) {
if (!_supportsComboFeedback(gift)) {
return;
}
setState(() {
_showComboFeedback = true;
});
_comboFeedbackController.forward(from: 0);
}
bool _supportsComboFeedback(SocialChatGiftRes gift) {
final giftTab = (gift.giftTab ?? '').trim();
return giftTab == "LUCK" ||
giftTab == SCGiftType.LUCKY_GIFT.name ||
giftTab == SCGiftType.CP.name ||
giftTab == SCGiftType.MAGIC.name;
}
Widget _buildSendButton() {
return SCGiftComboSendButton(
label: SCAppLocalizations.of(context)!.send,
onPressed: giveGifts,
showCountdown: _showComboFeedback,
countdownAnimation: _comboFeedbackController,
width: 96.w,
);
}
/// giftType转换为字符串类型
String _giftTypeToString(int giftType) {
switch (giftType) {
@ -1364,7 +1541,11 @@ class _GiftPageState extends State<GiftPage>
}
_buildGiftHead() {
if (giftType == 1 || giftType == 2 || giftType == 3 || giftType == 5) {
if (giftType == 2 || giftType == 3) {
return Container();
}
if (giftType == 1 || giftType == 5) {
//
String basePath = _strategy.getGiftPageActivityGiftHeadBackground(
_giftTypeToString(giftType),
@ -1375,15 +1556,15 @@ class _GiftPageState extends State<GiftPage>
if (SCGlobalConfig.lang == "ar") {
// _ar
if (basePath.endsWith('.png')) {
imagePath = basePath.substring(0, basePath.length - 4) + '_ar.png';
imagePath = '${basePath.substring(0, basePath.length - 4)}_ar.png';
} else {
imagePath = basePath + '_ar';
imagePath = '${basePath}_ar';
}
} else {
if (basePath.endsWith('.png')) {
imagePath = basePath.substring(0, basePath.length - 4) + '_en.png';
imagePath = '${basePath.substring(0, basePath.length - 4)}_en.png';
} else {
imagePath = basePath + '_en';
imagePath = '${basePath}_en';
}
}
@ -1446,14 +1627,12 @@ class _GiftPageState extends State<GiftPage>
}
class CheckNumber extends StatelessWidget {
final Function(int) onNumberChanged;
late BuildContext context;
final ValueChanged<int> onNumberChanged;
CheckNumber({super.key, required this.onNumberChanged});
const CheckNumber({super.key, required this.onNumberChanged});
@override
Widget build(BuildContext context) {
this.context = context;
return Container(
alignment: AlignmentDirectional.topEnd,
margin: EdgeInsets.only(right: width(22)),
@ -1516,6 +1695,89 @@ class CheckNumber extends StatelessWidget {
}
}
class _GiftSendRequest {
const _GiftSendRequest({
required this.acceptUserIds,
required this.acceptUsers,
required this.gift,
required this.quantity,
required this.roomId,
required this.roomAccount,
required this.isLuckyGiftRequest,
});
final List<String> acceptUserIds;
final List<MicRes> acceptUsers;
final SocialChatGiftRes gift;
final int quantity;
final String roomId;
final String roomAccount;
final bool isLuckyGiftRequest;
String get requestName => isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
String get batchKey {
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
return '${isLuckyGiftRequest ? "lucky" : "gift"}'
'|${gift.id ?? ""}'
'|$roomId'
'|${sortedAcceptUserIds.join(",")}';
}
}
class _PendingGiftSendBatch {
_PendingGiftSendBatch({
required this.batchKey,
required this.acceptUserIds,
required this.acceptUsers,
required this.gift,
required this.quantity,
required this.roomId,
required this.roomAccount,
required this.isLuckyGiftRequest,
required this.readyAt,
});
factory _PendingGiftSendBatch.fromRequest(
_GiftSendRequest request, {
required DateTime readyAt,
}) {
return _PendingGiftSendBatch(
batchKey: request.batchKey,
acceptUserIds: List<String>.from(request.acceptUserIds),
acceptUsers: List<MicRes>.from(request.acceptUsers),
gift: request.gift,
quantity: request.quantity,
roomId: request.roomId,
roomAccount: request.roomAccount,
isLuckyGiftRequest: request.isLuckyGiftRequest,
readyAt: readyAt,
);
}
final String batchKey;
final List<String> acceptUserIds;
final List<MicRes> acceptUsers;
final SocialChatGiftRes gift;
int quantity;
final String roomId;
final String roomAccount;
final bool isLuckyGiftRequest;
DateTime readyAt;
_GiftSendRequest toRequest() {
return _GiftSendRequest(
acceptUserIds: List<String>.from(acceptUserIds),
acceptUsers: List<MicRes>.from(acceptUsers),
gift: gift,
quantity: quantity,
roomId: roomId,
roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest,
);
}
}
class HeadSelect {
bool isSelect = false;
MicRes? mic;

View File

@ -1,149 +1,151 @@
import 'package:flutter/cupertino.dart';
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/login_res.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
class AllChatPage extends StatefulWidget {
@override
_AllChatPageState createState() => _AllChatPageState();
}
class _AllChatPageState extends State<AllChatPage> {
ScrollController _controller = ScrollController();
bool showGoBottom = true;
bool _isDisposed = false;
List<Msg> _msgList = [];
late RtmProvider provider;
@override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false);
var msgList = provider.roomAllMsgList;
provider.msgAllListener = _onNewMsg;
_msgList.addAll(msgList ??= []);
// 使
_initScrollPosition();
}
void _scrollListener() {
final position = _controller.position;
// 5
const tolerance = 10.0;
//
final isAtBottom = position.pixels < position.minScrollExtent + tolerance;
//
// final isAtTop = position.pixels >= position.maxScrollExtent - tolerance;
if (isAtBottom != showGoBottom) {
showGoBottom = isAtBottom;
if (!_isDisposed) {
setState(() {});
}
}
}
//
void _initScrollPosition() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_isDisposed) return;
_controller.jumpTo(0.0);
});
}
_onNewMsg(Msg msg) {
if (_isDisposed) return; //
if (msg.groupId == "-1000") {
///
_msgList.clear();
setState(() {});
return;
}
if (!_msgList.contains(msg)) {
setState(() {
_msgList.insert(0, msg);
});
_controller.jumpTo(0.0);
}
}
@override
void dispose() {
_isDisposed = true;
provider.msgAllListener = null;
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomLeft,
children: [
ListView.builder(
reverse: true,
controller: _controller,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return MsgItem(
msg: _msgList[index],
onClick: (SocialChatUserProfile? user) {
Provider.of<RtcProvider>(context, listen: false).clickSite(
Provider.of<RtcProvider>(
context,
listen: false,
).userOnMaiInIndex(user?.id ?? ""),
clickUser: user,
);
},
);
},
itemCount: _msgList.length,
),
Visibility(
visible: !showGoBottom,
child: GestureDetector(
onTap: () {
if (_controller.hasClients) {
_controller.jumpTo(0.0);
}
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: width(15)),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(52),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
SCAppLocalizations.of(context)!.scrollToTheBottom,
style: TextStyle(fontSize: 10.sp),
),
SizedBox(width: 4.w),
Icon(Icons.chevron_right, size: 10.w),
],
),
),
),
),
],
),
);
}
}
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/login_res.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
class AllChatPage extends StatefulWidget {
const AllChatPage({super.key});
@override
State<AllChatPage> createState() => _AllChatPageState();
}
class _AllChatPageState extends State<AllChatPage> {
final ScrollController _controller = ScrollController();
bool showGoBottom = true;
bool _isDisposed = false;
final List<Msg> _msgList = [];
late RtmProvider provider;
@override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false);
final msgList = provider.roomAllMsgList;
provider.msgAllListener = _onNewMsg;
_msgList.addAll(msgList);
// 使
_initScrollPosition();
}
void _scrollListener() {
final position = _controller.position;
// 5
const tolerance = 10.0;
//
final isAtBottom = position.pixels < position.minScrollExtent + tolerance;
//
// final isAtTop = position.pixels >= position.maxScrollExtent - tolerance;
if (isAtBottom != showGoBottom) {
showGoBottom = isAtBottom;
if (!_isDisposed) {
setState(() {});
}
}
}
//
void _initScrollPosition() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_isDisposed) return;
_controller.jumpTo(0.0);
});
}
_onNewMsg(Msg msg) {
if (_isDisposed) return; //
if (msg.groupId == "-1000") {
///
_msgList.clear();
setState(() {});
return;
}
setState(() {
if (_msgList.contains(msg)) {
_msgList.remove(msg);
}
_msgList.insert(0, msg);
});
_controller.jumpTo(0.0);
}
@override
void dispose() {
_isDisposed = true;
provider.msgAllListener = null;
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomLeft,
children: [
ListView.builder(
reverse: true,
controller: _controller,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return MsgItem(
msg: _msgList[index],
onClick: (SocialChatUserProfile? user) {
Provider.of<RtcProvider>(context, listen: false).clickSite(
Provider.of<RtcProvider>(
context,
listen: false,
).userOnMaiInIndex(user?.id ?? ""),
clickUser: user,
);
},
);
},
itemCount: _msgList.length,
),
Visibility(
visible: !showGoBottom,
child: GestureDetector(
onTap: () {
if (_controller.hasClients) {
_controller.jumpTo(0.0);
}
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: width(15)),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(52),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
SCAppLocalizations.of(context)!.scrollToTheBottom,
style: TextStyle(fontSize: 10.sp),
),
SizedBox(width: 4.w),
Icon(Icons.chevron_right, size: 10.w),
],
),
),
),
),
],
),
);
}
}

View File

@ -1,141 +1,143 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
class GiftChatPage extends StatefulWidget {
@override
_GiftChatPageState createState() => _GiftChatPageState();
}
class _GiftChatPageState extends State<GiftChatPage> {
ScrollController _controller = ScrollController();
bool showGoBottom = true;
bool _isDisposed = false;
List<Msg> _msgList = [];
late RtmProvider provider;
@override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false);
var msgList = provider.roomGiftMsgList;
provider.msgGiftListener = _onNewMsg;
_msgList.addAll(msgList ??= []);
// 使
_initScrollPosition();
}
void _scrollListener() {
final position = _controller.position;
// 5
const tolerance = 10.0;
//
final isAtBottom = position.pixels < position.minScrollExtent + tolerance;
//
// final isAtTop = position.pixels >= position.maxScrollExtent - tolerance;
if (isAtBottom != showGoBottom) {
showGoBottom = isAtBottom;
if (!_isDisposed) {
setState(() {});
}
}
}
//
void _initScrollPosition() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_isDisposed) return;
_controller.jumpTo(0.0);
});
}
_onNewMsg(Msg msg) {
if (_isDisposed) return; //
if (msg.groupId == "-1000") {
///
_msgList.clear();
setState(() {});
return;
}
if (!_msgList.contains(msg)) {
setState(() {
_msgList.insert(0, msg);
});
_controller.jumpTo(0.0);
}
}
@override
void dispose() {
_isDisposed = true;
provider.msgGiftListener = null;
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomLeft,
children: [
ListView.builder(
reverse: true,
controller: _controller,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return MsgItem(
msg: _msgList[index],
onClick: (SocialChatUserProfile? user) {},
);
},
itemCount: _msgList.length,
),
Visibility(
visible: !showGoBottom,
child: GestureDetector(
onTap: () {
if (_controller.hasClients) {
_controller.jumpTo(0.0);
}
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: width(15)),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(52),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
SCAppLocalizations.of(context)!.scrollToTheBottom,
style: TextStyle(fontSize: 10.sp),
),
SizedBox(width: 4.w),
Icon(Icons.chevron_right, size: 10.w),
],
),
),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
class GiftChatPage extends StatefulWidget {
const GiftChatPage({super.key});
@override
State<GiftChatPage> createState() => _GiftChatPageState();
}
class _GiftChatPageState extends State<GiftChatPage> {
final ScrollController _controller = ScrollController();
bool showGoBottom = true;
bool _isDisposed = false;
final List<Msg> _msgList = [];
late RtmProvider provider;
@override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
provider = Provider.of<RtmProvider>(context, listen: false);
final msgList = provider.roomGiftMsgList;
provider.msgGiftListener = _onNewMsg;
_msgList.addAll(msgList);
// 使
_initScrollPosition();
}
void _scrollListener() {
final position = _controller.position;
// 5
const tolerance = 10.0;
//
final isAtBottom = position.pixels < position.minScrollExtent + tolerance;
//
// final isAtTop = position.pixels >= position.maxScrollExtent - tolerance;
if (isAtBottom != showGoBottom) {
showGoBottom = isAtBottom;
if (!_isDisposed) {
setState(() {});
}
}
}
//
void _initScrollPosition() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_isDisposed) return;
_controller.jumpTo(0.0);
});
}
_onNewMsg(Msg msg) {
if (_isDisposed) return; //
if (msg.groupId == "-1000") {
///
_msgList.clear();
setState(() {});
return;
}
setState(() {
if (_msgList.contains(msg)) {
_msgList.remove(msg);
}
_msgList.insert(0, msg);
});
_controller.jumpTo(0.0);
}
@override
void dispose() {
_isDisposed = true;
provider.msgGiftListener = null;
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomLeft,
children: [
ListView.builder(
reverse: true,
controller: _controller,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return MsgItem(
msg: _msgList[index],
onClick: (SocialChatUserProfile? user) {},
);
},
itemCount: _msgList.length,
),
Visibility(
visible: !showGoBottom,
child: GestureDetector(
onTap: () {
if (_controller.hasClients) {
_controller.jumpTo(0.0);
}
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: width(15)),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(52),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
SCAppLocalizations.of(context)!.scrollToTheBottom,
style: TextStyle(fontSize: 10.sp),
),
SizedBox(width: 4.w),
Icon(Icons.chevron_right, size: 10.w),
],
),
),
),
),
],
),
);
}
}

View File

@ -13,6 +13,7 @@ import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
import 'package:yumi/services/gift/gift_animation_manager.dart';
import 'package:yumi/services/gift/gift_system_manager.dart';
import 'package:yumi/services/audio/rtm_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/widgets/room/anim/l_gift_animal_view.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
@ -40,12 +41,16 @@ class VoiceRoomPage extends StatefulWidget {
class _VoiceRoomPageState extends State<VoiceRoomPage>
with SingleTickerProviderStateMixin {
static const Duration _luckyGiftComboWindow = Duration(seconds: 3);
late TabController _tabController;
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
final List<Widget> _tabs = [];
late StreamSubscription _subscription;
final RoomGiftSeatFlightController _giftSeatFlightController =
RoomGiftSeatFlightController();
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
<String, _LuckyGiftComboSession>{};
@override
void initState() {
@ -64,6 +69,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
context,
listen: false,
).toggleGiftAnimationVisibility(false);
_clearLuckyGiftComboSessions();
_giftSeatFlightController.clear();
}
});
@ -75,6 +81,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) {
rtmProvider.msgFloatingGiftListener = null;
}
_clearLuckyGiftComboSessions();
_giftSeatFlightController.clear();
_tabController.dispose(); //
_subscription.cancel();
@ -281,6 +288,11 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
final targetUserId = _resolveGiftTargetUserId(msg);
final isLuckyGift = _isLuckyGiftMessage(msg);
if (isLuckyGift) {
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId);
return;
}
if (_shouldPlaySeatFlightGiftAnimation(msg) && targetUserId != null) {
_giftSeatFlightController.enqueue(
RoomGiftSeatFlightRequest(
@ -293,6 +305,82 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
}
}
bool _isLuckyGiftMessage(Msg msg) {
final giftTab = (msg.gift?.giftTab ?? '').trim();
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
}
void _handleLuckyGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
final quantity = (msg.number ?? 0).floor();
if (quantity <= 0) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions.putIfAbsent(
sessionKey,
() => _LuckyGiftComboSession(),
);
session.totalCount += quantity;
final highestMilestone =
SocialChatGiftSystemManager.resolveHighestReachedLuckGiftMilestone(
session.totalCount,
);
if (highestMilestone != null &&
highestMilestone > session.highestPlayedMilestone &&
SCGlobalConfig.isLuckGiftSpecialEffects) {
final effectPath =
SocialChatGiftSystemManager.resolveLuckGiftComboEffectPath(
highestMilestone,
);
if (effectPath != null && effectPath.isNotEmpty) {
SCGiftVapSvgaManager().play(effectPath, priority: 200);
session.highestPlayedMilestone = highestMilestone;
}
}
if (_shouldPlaySeatFlightGiftAnimation(msg) &&
targetUserId != null &&
giftPhoto.isNotEmpty) {
session.pendingFlightRequest = RoomGiftSeatFlightRequest(
imagePath: giftPhoto,
targetUserId: targetUserId,
beginSize: 96.w,
endSize: 28.w,
);
}
session.flushTimer?.cancel();
session.flushTimer = Timer(_luckyGiftComboWindow, () {
if (!mounted) {
return;
}
final activeSession = _luckyGiftComboSessions.remove(sessionKey);
final pendingFlightRequest = activeSession?.pendingFlightRequest;
activeSession?.dispose();
if (pendingFlightRequest != null) {
_giftSeatFlightController.enqueue(pendingFlightRequest);
}
});
}
String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) {
final senderId = (msg.user?.id ?? '').trim();
final giftId = (msg.gift?.id ?? '').trim();
return '$giftId|$senderId|${targetUserId ?? ""}';
}
void _clearLuckyGiftComboSessions() {
for (final session in _luckyGiftComboSessions.values) {
session.dispose();
}
_luckyGiftComboSessions.clear();
}
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
final gift = msg.gift;
if (gift == null) {
@ -355,3 +443,14 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
return null;
}
}
class _LuckyGiftComboSession {
Timer? flushTimer;
int totalCount = 0;
int highestPlayedMilestone = 0;
RoomGiftSeatFlightRequest? pendingFlightRequest;
void dispose() {
flushTimer?.cancel();
}
}

View File

@ -69,6 +69,8 @@ typedef OnMessageRecvC2CReadListener = Function(List<String> messageIDList);
typedef RtmProvider = RealTimeMessagingManager;
class RealTimeMessagingManager extends ChangeNotifier {
static const int _giftComboMergeWindowMs = 3000;
BuildContext? context;
void _giftFxLog(String message) {
@ -856,6 +858,17 @@ class RealTimeMessagingManager extends ChangeNotifier {
///
addMsg(Msg msg) {
final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg);
if (mergedGiftMsg != null) {
msgAllListener?.call(mergedGiftMsg);
msgGiftListener?.call(mergedGiftMsg);
if (msg.type == SCRoomMsgType.gift) {
msgFloatingGiftListener?.call(msg);
}
notifyListeners();
return;
}
roomAllMsgList.insert(0, msg);
if (roomAllMsgList.length > 250) {
print('大于200条消息');
@ -891,6 +904,59 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
}
Msg? _mergeGiftMessageIfNeeded(Msg incoming) {
if (incoming.type != SCRoomMsgType.gift &&
incoming.type != SCRoomMsgType.luckGiftAnimOther) {
return null;
}
final mergeTarget = _findMergeableGiftMessage(incoming);
if (mergeTarget == null) {
return null;
}
mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0);
mergeTarget.time = DateTime.now().millisecondsSinceEpoch;
if ((incoming.msg ?? "").trim().isNotEmpty) {
mergeTarget.msg = incoming.msg;
}
_moveMessageToFront(roomGiftMsgList, mergeTarget);
_moveMessageToFront(roomAllMsgList, mergeTarget);
return mergeTarget;
}
Msg? _findMergeableGiftMessage(Msg incoming) {
final now = DateTime.now().millisecondsSinceEpoch;
for (final existing in roomGiftMsgList) {
if ((existing.time ?? 0) <= 0 ||
now - (existing.time ?? 0) > _giftComboMergeWindowMs) {
continue;
}
if (_isSameGiftComboMessage(existing, incoming)) {
return existing;
}
}
return null;
}
bool _isSameGiftComboMessage(Msg existing, Msg incoming) {
return existing.type == incoming.type &&
existing.groupId == incoming.groupId &&
existing.user?.id == incoming.user?.id &&
existing.toUser?.id == incoming.toUser?.id &&
existing.gift?.id == incoming.gift?.id;
}
void _moveMessageToFront(List<Msg> messages, Msg target) {
final index = messages.indexOf(target);
if (index <= 0) {
return;
}
messages.removeAt(index);
messages.insert(0, target);
}
bool isLogout = false;
logout() async {

View File

@ -8,6 +8,65 @@ import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
typedef GiftProvider = SocialChatGiftSystemManager;
class SocialChatGiftSystemManager extends ChangeNotifier {
static const Map<int, String> _luckGiftComboEffectAssets = {
10: "sc_images/room/anim/luck_gift/luck_gift_combo_count_10.svga",
30: "sc_images/room/anim/luck_gift/luck_gift_combo_count_30.svga",
50: "sc_images/room/anim/luck_gift/luck_gift_combo_count_50.svga",
66: "sc_images/room/anim/luck_gift/luck_gift_combo_count_66.svga",
100: "sc_images/room/anim/luck_gift/luck_gift_combo_count_100.svga",
300: "sc_images/room/anim/luck_gift/luck_gift_combo_count_300.svga",
400: "sc_images/room/anim/luck_gift/luck_gift_combo_count_400.svga",
666: "sc_images/room/anim/luck_gift/luck_gift_combo_count_666.svga",
777: "sc_images/room/anim/luck_gift/luck_gift_combo_count_777.svga",
2000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_2000.svga",
3000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_3000.svga",
5000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_5000.svga",
6000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_6000.svga",
7000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_7000.svga",
8000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_8000.svga",
10000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_10000.svga",
};
static const List<int> _luckGiftMilestones = <int>[
10,
20,
30,
50,
66,
88,
100,
200,
300,
400,
500,
666,
777,
888,
1000,
1500,
2000,
3000,
5000,
10000,
15000,
20000,
25000,
30000,
35000,
40000,
45000,
50000,
55000,
60000,
65000,
70000,
75000,
80000,
85000,
90000,
95000,
100000,
];
///
bool hideLGiftAnimal = false;
@ -129,51 +188,81 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
void modifyLuckyGiftCount(
num n,
bool isManyPeople,
bool manyPeople,
MicRes first,
SocialChatGiftRes? checkedGift,
) {
this.number = number + n;
this.isManyPeople = isManyPeople;
this.toUser = first;
this.gift = checkedGift;
number = number + n;
isManyPeople = manyPeople;
toUser = first;
gift = checkedGift;
giftAnimSize = 1.4;
notifyListeners();
startGiftAnimation();
}
void updateLuckyRewardAmount(num n) {
this.awardAmount = n;
this.luckGiftObtainCoins = luckGiftObtainCoins + n;
this.obtainCoinsAnimSize = 1.4;
this.awardAmountAnimSize = 1.4;
awardAmount = n;
luckGiftObtainCoins = luckGiftObtainCoins + n;
obtainCoinsAnimSize = 1.4;
awardAmountAnimSize = 1.4;
notifyListeners();
}
void startGiftAnimation() {
isPlayed.forEach((k, v) {
if (k < number + 1 && !v) {
playVisualEffect(k);
}
});
final milestone = resolveHighestReachedLuckGiftMilestone(number);
if (milestone == null || (isPlayed[milestone] ?? false)) {
return;
}
playVisualEffect(milestone);
_markMilestonesPlayedUpTo(milestone);
}
void playVisualEffect(num n) {
if (!(isPlayed[n] ?? false)) {
if (SCGlobalConfig.isLuckGiftSpecialEffects) {
if (n > 9999) {
SCGiftVapSvgaManager().play(
"sc_images/room/anim/luck_gift_count_5000_mor.mp4",
priority: 200,
);
} else {
SCGiftVapSvgaManager().play(
"sc_images/room/anim/luck_gift_count_$n.mp4",
priority: 200,
);
final comboEffectPath = resolveLuckGiftComboEffectPath(n);
if (comboEffectPath != null) {
SCGiftVapSvgaManager().play(comboEffectPath, priority: 200);
}
}
}
isPlayed[n] = true;
}
void _markMilestonesPlayedUpTo(int milestone) {
for (final threshold in _luckGiftMilestones) {
if (threshold > milestone) {
break;
}
isPlayed[threshold] = true;
}
}
static int? resolveHighestReachedLuckGiftMilestone(num count) {
final normalizedCount = count.floor();
int? highest;
for (final milestone in _luckGiftMilestones) {
if (milestone > normalizedCount) {
break;
}
highest = milestone;
}
return highest;
}
static String? resolveLuckGiftComboEffectPath(num count) {
if (count % 1 != 0) {
return null;
}
final normalizedCount = count.toInt();
final comboEffectPath = _luckGiftComboEffectAssets[normalizedCount];
if (comboEffectPath != null) {
return comboEffectPath;
}
if (normalizedCount > 9999) {
return "sc_images/room/anim/luck_gift_count_5000_mor.mp4";
}
return "sc_images/room/anim/luck_gift_count_$normalizedCount.mp4";
}
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
class SCGiftComboSendButton extends StatefulWidget {
const SCGiftComboSendButton({
super.key,
required this.label,
required this.onPressed,
required this.showCountdown,
this.countdownAnimation,
this.width = 96,
});
final String label;
final VoidCallback onPressed;
final bool showCountdown;
final Animation<double>? countdownAnimation;
final double width;
@override
State<SCGiftComboSendButton> createState() => _SCGiftComboSendButtonState();
}
class _SCGiftComboSendButtonState extends State<SCGiftComboSendButton> {
static const Duration _pressScaleDuration = Duration(milliseconds: 90);
static const double _pressedScale = 0.96;
bool _pressed = false;
void _setPressed(bool value) {
if (_pressed == value) {
return;
}
setState(() {
_pressed = value;
});
}
@override
Widget build(BuildContext context) {
final animation =
widget.countdownAnimation ?? const AlwaysStoppedAnimation<double>(1);
final baseColor = SocialChatTheme.primaryLight;
final countdownLightColor = Color.lerp(baseColor, Colors.white, 0.18)!;
final countdownDeepColor = Color.lerp(baseColor, Colors.white, 0.04)!;
final borderRadius = BorderRadius.circular(5);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) => _setPressed(true),
onTapUp: (_) => _setPressed(false),
onTapCancel: () => _setPressed(false),
onTap: widget.onPressed,
child: AnimatedScale(
scale: _pressed ? _pressedScale : 1,
duration: _pressScaleDuration,
curve: Curves.easeOutCubic,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
final progress =
widget.showCountdown
? (1 - animation.value).clamp(0.0, 1.0)
: 0.0;
return SizedBox(
width: widget.width,
child: ClipRRect(
borderRadius: borderRadius,
child: DecoratedBox(
decoration: BoxDecoration(
color: baseColor,
borderRadius: borderRadius,
),
child: LayoutBuilder(
builder: (context, constraints) {
final countdownWidth = constraints.maxWidth * progress;
return Stack(
alignment: Alignment.center,
children: [
if (countdownWidth > 0)
Positioned(
left: 0,
top: 0,
bottom: 0,
width: countdownWidth,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerRight,
end: Alignment.centerLeft,
colors: [
countdownLightColor,
countdownDeepColor,
],
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(
vertical: 8.w,
horizontal: 20.w,
),
child: Text(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w400,
),
),
),
],
);
},
),
),
),
);
},
),
),
);
}
}

View File

@ -1,115 +1,139 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:provider/provider.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
class LuckGiftNomorAnimWidget extends StatefulWidget {
@override
_LuckGiftNomorAnimWidgetState createState() =>
_LuckGiftNomorAnimWidgetState();
}
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
@override
void initState() {
super.initState();
Provider.of<RtmProvider>(context, listen: false).showLuckGiftBigHead = true;
}
dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Consumer<RtmProvider>(
builder: (context, provider, child) {
return provider.currentPlayingLuckGift != null
? Container(
height: 380.w,
margin: EdgeInsets.only(top: 10.w),
child: Stack(
children: [
Image.asset(
"sc_images/room/sc_icon_luck_gift_nomore.webp",
fit: BoxFit.fitWidth,
),
provider.showLuckGiftBigHead?Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 125.w),
netImage(
url:
provider
.currentPlayingLuckGift
?.data
?.userAvatar ??
"",
shape: BoxShape.circle,
height: 105.w,
width: 105.w,
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"sc_images/general/sc_icon_jb.png",
height: 35.w,
),
SizedBox(width: 3.w),
Image.asset(
"sc_images/general/sc_icon_game_numxx.png",
height: 20.w,
),
Transform.translate(
offset: Offset(-5, 0),
child: buildNumForGame(
(provider
.currentPlayingLuckGift
?.data
?.awardAmount ??
0) >
9999
? "${((provider.currentPlayingLuckGift?.data?.awardAmount ?? 0) / 1000).toStringAsFixed(0)}k"
: "${(provider.currentPlayingLuckGift?.data?.awardAmount ?? 0)}",
size: 22.w,
),
),
],
),
SizedBox(height: 12.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: 15.w),
buildNumForGame(
"${provider.currentPlayingLuckGift?.data?.multiple ?? 0}",
size: 22.w,
),
Transform.translate(
offset: Offset(-3, 3),
child: Image.asset(
SCGlobalConfig.lang=="ar"? "sc_images/room/sc_icon_times_text_ar.png":"sc_images/room/sc_icon_times_text_en.png",
height: 12.w,
),
),
],
),
],
):Container(),
],
),
)
: Container();
},
),
);
}
}
import 'package:flutter/cupertino.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:provider/provider.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
class LuckGiftNomorAnimWidget extends StatefulWidget {
static const String _rewardFrameAssetPath =
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
static const String _rewardFrameFallbackAssetPath =
"sc_images/room/sc_icon_luck_gift_nomore.webp";
const LuckGiftNomorAnimWidget({super.key});
@override
State<LuckGiftNomorAnimWidget> createState() =>
_LuckGiftNomorAnimWidgetState();
}
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
@override
void initState() {
super.initState();
Provider.of<RtmProvider>(context, listen: false).showLuckGiftBigHead = true;
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Consumer<RtmProvider>(
builder: (context, provider, child) {
return provider.currentPlayingLuckGift != null
? Container(
height: 380.w,
margin: EdgeInsets.only(top: 10.w),
child: Stack(
children: [
SCSvgaAssetWidget(
key: ValueKey<String>(
'${provider.currentPlayingLuckGift?.data?.sendUserId ?? ""}'
'|${provider.currentPlayingLuckGift?.data?.acceptUserId ?? ""}'
'|${provider.currentPlayingLuckGift?.data?.awardAmount ?? 0}'
'|${provider.currentPlayingLuckGift?.data?.multiple ?? 0}',
),
assetPath: LuckGiftNomorAnimWidget._rewardFrameAssetPath,
width: ScreenUtil().screenWidth,
height: 380.w,
fit: BoxFit.fitWidth,
allowDrawingOverflow: true,
fallback: Image.asset(
LuckGiftNomorAnimWidget._rewardFrameFallbackAssetPath,
fit: BoxFit.fitWidth,
),
),
provider.showLuckGiftBigHead
? Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 125.w),
netImage(
url:
provider
.currentPlayingLuckGift
?.data
?.userAvatar ??
"",
shape: BoxShape.circle,
height: 105.w,
width: 105.w,
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"sc_images/general/sc_icon_jb.png",
height: 35.w,
),
SizedBox(width: 3.w),
Image.asset(
"sc_images/general/sc_icon_game_numxx.png",
height: 20.w,
),
Transform.translate(
offset: Offset(-5, 0),
child: buildNumForGame(
(provider
.currentPlayingLuckGift
?.data
?.awardAmount ??
0) >
9999
? "${((provider.currentPlayingLuckGift?.data?.awardAmount ?? 0) / 1000).toStringAsFixed(0)}k"
: "${(provider.currentPlayingLuckGift?.data?.awardAmount ?? 0)}",
size: 22.w,
),
),
],
),
SizedBox(height: 12.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: 15.w),
buildNumForGame(
"${provider.currentPlayingLuckGift?.data?.multiple ?? 0}",
size: 22.w,
),
Transform.translate(
offset: Offset(-3, 3),
child: Image.asset(
SCGlobalConfig.lang == "ar"
? "sc_images/room/sc_icon_times_text_ar.png"
: "sc_images/room/sc_icon_times_text_en.png",
height: 12.w,
),
),
],
),
],
)
: Container(),
],
),
)
: Container();
},
),
);
}
}

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
from contextlib import contextmanager
import datetime as dt
import hashlib
import json
@ -10,6 +11,7 @@ import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
@ -54,14 +56,16 @@ def ensure_clean_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def copy_file(src: Path, dest: Path) -> dict[str, object]:
def copy_file(src: Path, dest: Path, *, include_sha256: bool = True) -> dict[str, object]:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
return {
result: dict[str, object] = {
"path": str(dest.relative_to(ROOT)),
"sizeBytes": dest.stat().st_size,
"sha256": sha256_of(dest),
}
if include_sha256:
result["sha256"] = sha256_of(dest)
return result
def copy_tree(src: Path, dest: Path) -> None:
@ -69,8 +73,54 @@ def copy_tree(src: Path, dest: Path) -> None:
shutil.copytree(src, dest, dirs_exist_ok=True)
def append_common_flutter_args(command: list[str], args: argparse.Namespace) -> None:
command.extend(["--release", f"--target={args.target}"])
def first_existing_path(*candidates: Path) -> Path:
for candidate in candidates:
if candidate.exists():
return candidate
raise FileNotFoundError("No build output found in: " + ", ".join(str(candidate) for candidate in candidates))
def timings_bucket(manifest: dict[str, object]) -> dict[str, object]:
timings = manifest.get("timings")
if not isinstance(timings, dict):
timings = {"stages": []}
manifest["timings"] = timings
stages = timings.get("stages")
if not isinstance(stages, list):
timings["stages"] = []
return timings
@contextmanager
def timed_stage(manifest: dict[str, object], key: str, label: str) -> dict[str, object]:
stage_started_at = dt.datetime.now()
stage_started_perf = time.perf_counter()
stage: dict[str, object] = {
"key": key,
"label": label,
"startedAt": stage_started_at.isoformat(timespec="seconds"),
}
timings_bucket(manifest)["stages"].append(stage)
print(f"[stage:start] {label}", flush=True)
try:
yield stage
except Exception as exc:
stage["status"] = "failed"
stage["error"] = str(exc)
raise
else:
stage["status"] = "succeeded"
finally:
duration_seconds = round(time.perf_counter() - stage_started_perf, 3)
stage["endedAt"] = dt.datetime.now().isoformat(timespec="seconds")
stage["durationSeconds"] = duration_seconds
print(f"[stage:end] {label} ({duration_seconds:.1f}s)", flush=True)
def append_common_flutter_args(command: list[str], args: argparse.Namespace, *, build_mode: str = "release") -> None:
command.extend([f"--{build_mode}", f"--target={args.target}"])
if args.flavor:
command.extend(["--flavor", args.flavor])
@ -110,7 +160,8 @@ def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None
appbundle_cmd = ["flutter", "build", "appbundle"]
append_common_flutter_args(appbundle_cmd, args)
appbundle_cmd.append(f"--split-debug-info={android_symbols_dir.relative_to(ROOT)}")
run_command(appbundle_cmd)
with timed_stage(manifest, "android.googlePlayAab", "Android 谷歌发布包AAB"):
run_command(appbundle_cmd)
if profile == "full":
apk_cmd = [
@ -123,19 +174,19 @@ def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None
]
append_common_flutter_args(apk_cmd, args)
apk_cmd.append(f"--split-debug-info={android_symbols_dir.relative_to(ROOT)}")
run_command(apk_cmd)
with timed_stage(manifest, "android.releaseApks", "Android 多 ABI 正式 APK"):
run_command(apk_cmd)
elif profile == "local-arm64":
apk_cmd = [
"flutter",
"build",
"apk",
"--split-per-abi",
"--target-platform",
"android-arm64",
]
append_common_flutter_args(apk_cmd, args)
apk_cmd.append(f"--split-debug-info={android_symbols_dir.relative_to(ROOT)}")
run_command(apk_cmd)
append_common_flutter_args(apk_cmd, args, build_mode="debug")
with timed_stage(manifest, "android.localDebugApk", "Android 极速测试包Debug / ARM64"):
run_command(apk_cmd)
google_play_dir = android_output_dir / "google-play"
local_dir = android_output_dir / "local"
@ -144,28 +195,38 @@ def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None
artifact_prefix = f"{args.package_name}-v{args.build_name}-b{args.build_number}"
artifacts: dict[str, object] = {}
with timed_stage(manifest, "android.collectArtifacts", "整理 Android 产物"):
if profile in {"full", "google-play"}:
aab_src = ROOT / "build" / "app" / "outputs" / "bundle" / "release" / "app-release.aab"
artifacts["googlePlayAab"] = copy_file(aab_src, google_play_dir / f"{artifact_prefix}-google-play.aab")
if profile in {"full", "google-play"}:
aab_src = ROOT / "build" / "app" / "outputs" / "bundle" / "release" / "app-release.aab"
artifacts["googlePlayAab"] = copy_file(aab_src, google_play_dir / f"{artifact_prefix}-google-play.aab")
if profile == "full":
arm64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-release.apk"
artifacts["localArm64Apk"] = copy_file(arm64_src, local_dir / f"{artifact_prefix}-arm64-v8a.apk")
elif profile == "local-arm64":
arm64_src = first_existing_path(
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-debug.apk",
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-debug.apk",
)
artifacts["localArm64Apk"] = copy_file(
arm64_src,
local_dir / f"{artifact_prefix}-arm64-v8a-debug.apk",
include_sha256=False,
)
if profile in {"full", "local-arm64"}:
arm64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-release.apk"
artifacts["localArm64Apk"] = copy_file(arm64_src, local_dir / f"{artifact_prefix}-arm64-v8a.apk")
if profile == "full":
armv7_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-armeabi-v7a-release.apk"
x64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-x86_64-release.apk"
artifacts["localArmeabiV7aApk"] = copy_file(armv7_src, local_dir / f"{artifact_prefix}-armeabi-v7a.apk")
artifacts["testingX64Apk"] = copy_file(x64_src, testing_dir / f"{artifact_prefix}-x86_64-test.apk")
if profile == "full":
armv7_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-armeabi-v7a-release.apk"
x64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-x86_64-release.apk"
artifacts["localArmeabiV7aApk"] = copy_file(armv7_src, local_dir / f"{artifact_prefix}-armeabi-v7a.apk")
artifacts["testingX64Apk"] = copy_file(x64_src, testing_dir / f"{artifact_prefix}-x86_64-test.apk")
if android_symbols_dir.exists():
symbols_parent_dir = google_play_dir if profile in {"full", "google-play"} else local_dir
copy_tree(android_symbols_dir, symbols_parent_dir / "symbols")
artifacts["dartSymbolsDir"] = {
"path": str((symbols_parent_dir / "symbols").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (symbols_parent_dir / "symbols").rglob("*") if path.is_file()),
}
if android_symbols_dir.exists() and profile in {"full", "google-play"}:
symbols_parent_dir = google_play_dir if profile in {"full", "google-play"} else local_dir
copy_tree(android_symbols_dir, symbols_parent_dir / "symbols")
artifacts["dartSymbolsDir"] = {
"path": str((symbols_parent_dir / "symbols").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (symbols_parent_dir / "symbols").rglob("*") if path.is_file()),
}
manifest["android"] = artifacts
@ -185,7 +246,8 @@ def build_ios(args: argparse.Namespace, manifest: dict[str, object]) -> None:
else:
command.append("--no-codesign")
run_command(command)
with timed_stage(manifest, "ios.buildIpa", "iOS 构建与导出"):
run_command(command)
artifact_prefix = f"{args.package_name}-v{args.build_name}-b{args.build_number}"
artifacts: dict[str, object] = {}
@ -193,23 +255,24 @@ def build_ios(args: argparse.Namespace, manifest: dict[str, object]) -> None:
archive_src = ROOT / "build" / "ios" / "archive" / "Runner.xcarchive"
ipa_candidates = sorted((ROOT / "build" / "ios" / "ipa").glob("*.ipa"))
if archive_src.exists():
copy_tree(archive_src, ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive")
artifacts["archiveDir"] = {
"path": str((ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").rglob("*") if path.is_file()),
}
with timed_stage(manifest, "ios.collectArtifacts", "整理 iOS 产物"):
if archive_src.exists():
copy_tree(archive_src, ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive")
artifacts["archiveDir"] = {
"path": str((ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").rglob("*") if path.is_file()),
}
if ipa_candidates:
ipa_src = ipa_candidates[-1]
artifacts["ipa"] = copy_file(ipa_src, ios_output_dir / "ipa" / f"{artifact_prefix}.ipa")
if ipa_candidates:
ipa_src = ipa_candidates[-1]
artifacts["ipa"] = copy_file(ipa_src, ios_output_dir / "ipa" / f"{artifact_prefix}.ipa")
if ios_symbols_dir.exists():
copy_tree(ios_symbols_dir, ios_output_dir / "symbols")
artifacts["dartSymbolsDir"] = {
"path": str((ios_output_dir / "symbols").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "symbols").rglob("*") if path.is_file()),
}
if ios_symbols_dir.exists():
copy_tree(ios_symbols_dir, ios_output_dir / "symbols")
artifacts["dartSymbolsDir"] = {
"path": str((ios_output_dir / "symbols").relative_to(ROOT)),
"sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "symbols").rglob("*") if path.is_file()),
}
if not artifacts:
raise RuntimeError("iOS build finished but no archive or ipa artifact was found.")
@ -276,30 +339,48 @@ def main() -> int:
parser = create_argument_parser()
args = parser.parse_args()
args.output_dir = args.output_dir.resolve()
build_started_at = dt.datetime.now()
build_started_perf = time.perf_counter()
manifest_path = args.output_dir / "build_manifest.json"
args.output_dir.mkdir(parents=True, exist_ok=True)
manifest: dict[str, object] = {
"generatedAt": dt.datetime.now().isoformat(timespec="seconds"),
"packageName": args.package_name,
"platform": args.platform,
"buildName": args.build_name,
"buildNumber": args.build_number,
"target": args.target,
"flavor": args.flavor,
"outputDir": str(args.output_dir.relative_to(ROOT)),
"timings": {
"startedAt": build_started_at.isoformat(timespec="seconds"),
"stages": [],
},
}
if should_build_android(args.platform):
build_android(args, manifest)
if should_build_ios(args.platform):
build_ios(args, manifest)
manifest_path = args.output_dir / "build_manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"Artifacts copied to: {args.output_dir}")
print(f"Manifest written to: {manifest_path}")
return 0
exit_code = 0
try:
if should_build_android(args.platform):
build_android(args, manifest)
if should_build_ios(args.platform):
build_ios(args, manifest)
manifest["status"] = "succeeded"
print(f"Artifacts copied to: {args.output_dir}")
except Exception as exc:
manifest["status"] = "failed"
manifest["error"] = str(exc)
exit_code = 1
raise
finally:
timings = timings_bucket(manifest)
timings["endedAt"] = dt.datetime.now().isoformat(timespec="seconds")
timings["totalSeconds"] = round(time.perf_counter() - build_started_perf, 3)
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"Total build time: {timings['totalSeconds']:.1f}s", flush=True)
print(f"Manifest written to: {manifest_path}", flush=True)
return exit_code
if __name__ == "__main__":

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:yumi/app/config/app_config.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/modules/auth/account/sc_login_with_account_page.dart';
import 'package:yumi/modules/settings/language/language_page.dart';
import 'package:yumi/services/localization/localization_manager.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
AppConfig.initialize();
tearDown(() async {
DataPersistence.reset();
});
const locales = <Locale>[
Locale('en'),
Locale('ar'),
Locale('tr'),
Locale('bn'),
];
for (final locale in locales) {
group('Locale ${locale.languageCode}', () {
testWidgets('LanguagePage renders cleanly', (tester) async {
await _pumpLocalizedPage(
tester: tester,
locale: locale,
child: LanguagePage(),
);
final exceptions = _drainExceptions(tester);
expect(exceptions, isEmpty, reason: exceptions.join('\n\n'));
expect(
tester
.widget<MaterialApp>(find.byType(MaterialApp))
.locale
?.languageCode,
locale.languageCode,
);
});
testWidgets('SCLoginWithAccountPage renders cleanly', (tester) async {
await _pumpLocalizedPage(
tester: tester,
locale: locale,
child: const SCLoginWithAccountPage(),
);
final exceptions = _drainExceptions(tester);
expect(exceptions, isEmpty, reason: exceptions.join('\n\n'));
expect(
tester
.widget<MaterialApp>(find.byType(MaterialApp))
.locale
?.languageCode,
locale.languageCode,
);
});
});
}
}
Future<void> _pumpLocalizedPage({
required WidgetTester tester,
required Locale locale,
required Widget child,
}) async {
SharedPreferences.setMockInitialValues({'lang': locale.languageCode});
await DataPersistence.initialize();
await tester.binding.setSurfaceSize(const Size(390, 844));
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => LocalizationManager(),
child: ScreenUtilInit(
designSize: Size(SCScreen.designWidth, SCScreen.designHeight),
splitScreenMode: false,
minTextAdapt: true,
builder: (context, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
locale: locale,
localizationsDelegates: const [
SCAppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('ar'),
Locale('tr'),
Locale('bn'),
],
home: RepaintBoundary(
key: const ValueKey('capture_boundary'),
child: child,
),
);
},
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
}
List<Object> _drainExceptions(WidgetTester tester) {
final exceptions = <Object>[];
Object? exception;
while ((exception = tester.takeException()) != null) {
exceptions.add(exception!);
}
return exceptions;
}

View File

@ -13,6 +13,11 @@
- 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。
## 已完成模块
- 已继续优化幸运/CP 礼物连击体验:房间 `Gift/All` 消息面板现在会对短时间内同一发送者、同一目标、同一礼物的连续赠送做聚合更新,不再每点一次就追加一条新消息,而是复用同一条礼物播报并持续刷新成 `xN`同时礼物页底部发送按钮已补上本地连击反馈Lucky/CP/Magic 类礼物连续点击时会显示一条从右向左收缩的浅色倒计时渐变条,并同步累加当前连击数量,用户能更直观看到连击窗口是否还在持续。
- 已继续给发送端补齐连击请求聚合:礼物页当前会对 Lucky/CP/Magic 这类连击型礼物启用约 `200ms` 的本地批量窗口,用户在短时间内连续点击 `Send` 时,前端会先把同一礼物、同一目标集合、同一房间的点击数量累加到同一批次里,再统一发一次接口和一次房间 RTM 消息;这样高连击时不再按点击次数直冲 `/gift/batch``/gift/give/lucky-gift`,同时也避免本地回显和房间消息量被线性放大。
- 已继续收敛幸运礼物高连击时的播放策略:房间内幸运礼物现在按 3 秒连击会话聚合,连击期间只更新房内上飘与总次数,不再每次都立刻飞向麦位;同一轮连击结束后才会向目标麦位补播一次飞行动画,避免 `1000` 连击把静态礼物飞行动画排成超长队列;同时幸运礼物的档位特效已改为只命中当前会话累计数量对应的最高有效档位,不再把中间跨过的 `10/20/30/...` 全部补播一遍。
- 已定位到幸运礼物“中奖通知”使用的是房间页顶层 `LuckGiftNomorAnimWidget`,此前背景一直是静态 `sc_icon_luck_gift_nomore.webp`;当前已改为优先播放本地 `sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga`,并保留原 `webp` 作为失败兜底,这样幸运礼物中奖弹层会直接复用新的奖励边框动效资源。
- 已开始接入幸运礼物连击档位的新动效资源:桌面“幸运礼物相关”目录下的 SVGA 已统一导入到 `sc_images/room/anim/luck_gift/`,并按规范重命名为 `luck_gift_combo_count_10.svga / luck_gift_combo_count_666.svga / luck_gift_combo_count_10000.svga` 这一类统一英文命名;当前连击阈值触发仍复用 `GiftSystemManager.playVisualEffect()`,命中已提供素材的档位时优先播放新的本地 SVGA未提供素材的档位继续保留旧兜底逻辑避免影响现有幸运礼物连击链路。
- 已将语言房送礼链路接入新的“中心停留后飞向目标麦位”组件,但只对无自带特效的静态 PNG 礼物生效:当前带自身 `SVGA/MP4/VAP` 动画或被识别为全屏礼物特效的礼物保持原有播放逻辑不变;只有普通 PNG 礼物会额外触发“屏幕中央停留 -> 三连残影飞向被赠送麦位”的补充动效,避免和自带礼物特效重复叠播。
- 已继续收敛语言房送礼飞行动效的命中条件:上一版对普通礼物的过滤过严,既依赖 `giftPhoto` 必须显式以 `.png` 结尾,也会被部分礼物的 `special` 标记误伤,导致不少实际没有自带动画的礼物被提前跳过;当前已改为只排除真实带 `SVGA/MP4/VAP` 动画源的礼物,普通静态封面礼物即使是带 query 的图片 URL、或不是严格 `.png` 后缀,也会正常触发“中心停留 -> 飞向目标麦位”的补充动画。
- 已将语言房送礼飞行动画从房间页内部 `Stack` 提升到应用根层,挂载方式对齐现有 `SVGA/VAP` 礼物特效层:当前该动画会和 `VapPlusSvgaPlayer` 一样在 `main.dart` 的顶层 builder 中全屏绘制,因此不会再被房间内部聊天区、局部动效或页面层级压住,视觉上更靠前、更容易被用户看到。