Compare commits
18 Commits
fa516a94e1
...
85e6d88472
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e6d88472 | ||
|
|
915fa61f0a | ||
|
|
3bcbce71eb | ||
|
|
53dbacef91 | ||
|
|
c810e645dc | ||
|
|
4aae854b78 | ||
|
|
14de02c822 | ||
|
|
058a4d9c4e | ||
|
|
23e538cd0c | ||
|
|
5f79a8b523 | ||
|
|
36215ff4a3 | ||
|
|
a9877b2a9b | ||
|
|
01ba9d384b | ||
|
|
35c05a9123 | ||
|
|
85a1b01347 | ||
|
|
9a889e32cb | ||
|
|
e41cb3fb24 | ||
|
|
23748737a2 |
@ -6,9 +6,13 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
@ -130,6 +134,12 @@
|
||||
android:name="io.agora.rtc2.extensions.MediaProjectionMgr$LocalScreenSharingService"
|
||||
tools:node="remove" />
|
||||
|
||||
<service
|
||||
android:name=".VoiceRoomForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback|microphone"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="high_importance_channel" />
|
||||
|
||||
@ -5,6 +5,7 @@ import com.android.installreferrer.api.InstallReferrerStateListener
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.TimeZone
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var installReferrerClient: InstallReferrerClient? = null
|
||||
@ -20,6 +21,42 @@ class MainActivity : FlutterActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MOBILE_CONTEXT_CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getTimeZoneIdentifier" -> result.success(TimeZone.getDefault().id)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
VOICE_ROOM_FOREGROUND_CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
try {
|
||||
VoiceRoomForegroundService.start(
|
||||
applicationContext,
|
||||
call.argument<String>("roomName")
|
||||
)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("VOICE_ROOM_FOREGROUND_START_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
"stop" -> {
|
||||
try {
|
||||
VoiceRoomForegroundService.stop(applicationContext)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("VOICE_ROOM_FOREGROUND_STOP_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -79,5 +116,8 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
companion object {
|
||||
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
|
||||
private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context"
|
||||
private const val VOICE_ROOM_FOREGROUND_CHANNEL =
|
||||
"com.org.yumiparty/voice_room_foreground_service"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
package com.org.yumiparty
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
class VoiceRoomForegroundService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ensureNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val roomName = intent?.getStringExtra(EXTRA_ROOM_NAME)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: DEFAULT_ROOM_NAME
|
||||
startAsForeground(roomName)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startAsForeground(roomName: String) {
|
||||
val notification = buildNotification(roomName)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val serviceType =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
}
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
serviceType
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun buildNotification(roomName: String): Notification {
|
||||
val launchIntent = (
|
||||
packageManager.getLaunchIntentForPackage(packageName)
|
||||
?: Intent(this, MainActivity::class.java)
|
||||
).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or immutableFlag()
|
||||
)
|
||||
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, CHANNEL_ID)
|
||||
} else {
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
return builder
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("Yumi voice room")
|
||||
.setContentText(roomName)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(Notification.PRIORITY_LOW)
|
||||
.setCategory(Notification.CATEGORY_CALL)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
|
||||
return
|
||||
}
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Voice room",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Keeps voice rooms active while Yumi is in the background."
|
||||
setShowBadge(false)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun immutableFlag(): Int =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "yumi_voice_room"
|
||||
private const val NOTIFICATION_ID = 24031
|
||||
private const val EXTRA_ROOM_NAME = "room_name"
|
||||
private const val DEFAULT_ROOM_NAME = "Voice room is running"
|
||||
|
||||
fun start(context: Context, roomName: String?) {
|
||||
val intent = Intent(context, VoiceRoomForegroundService::class.java)
|
||||
.putExtra(EXTRA_ROOM_NAME, roomName)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -327,9 +327,47 @@
|
||||
"redEnvelopeRecTips2": "تم المطالبة بجميع المظاريف الحمراء.",
|
||||
"redEnvelopeRecTips3": "انتهى وقت جمع الظرف الأحمر!",
|
||||
"openTheTreasureChest": "افتح صندوق الكنز",
|
||||
"redEnvelopeRecTips1": "تم إيداع العملات المكتسبة في محفظتك.",
|
||||
"redEnvelope": "ظرف أحمر",
|
||||
"redEnvelopeTips1": "عملات:",
|
||||
"redEnvelopeRecTips1": "تم إيداع العملات المكتسبة في محفظتك.",
|
||||
"redEnvelope": "ظرف أحمر",
|
||||
"roomRedPacketGoldCoins": "عملات ذهبية",
|
||||
"roomRedPacketQuantity": "العدد",
|
||||
"roomRedPacketPickupTime": "وقت الاستلام",
|
||||
"roomRedPacketInMinutes": "بعد {1} دقائق",
|
||||
"roomRedPacketClaimNow": "استلم الآن",
|
||||
"roomRedPacketSendLuckyPack": "أرسل حزمة الحظ",
|
||||
"roomRedPacketSending": "جارٍ الإرسال...",
|
||||
"roomRedPacketSendSuccess": "تم إرسال حزمة الحظ",
|
||||
"roomRedPacketComingSoon": "قريباً",
|
||||
"roomRedPacketRulesTitle": "القواعد",
|
||||
"roomRedPacketRulesContent": "1. يمكنك إنفاق العملات لإرسال حزمة حظ. إذا فتح الآخرون حزمة الحظ الخاصة بك، يمكنهم الحصول على عدد عشوائي من العملات.\n\n2. بعد مرور ساعتين على إرسال حزمة الحظ، ستُعاد العملات التي لم تُستلم إلى محفظتك.\n\n3. بعد إرسال حزمة حظ على مستوى الخادم، سيظهر شريط حزمة حظ على مستوى الخادم، وستحصل على المزيد من الاهتمام.\n\n4. يمكنك إرسال %1$s حزمة حظ للغرفة يومياً.\n\n5. لا يمكن استلام حزم الحظ التي يرسلها مستخدمون من منطقة مختلفة.",
|
||||
"roomRedPacketHistoryTitle": "سجل إرسال حزم الحظ",
|
||||
"roomRedPacketHistoryItemTitle": "حزمة حظ ({1} مظروفاً)",
|
||||
"roomRedPacketHistoryStatusSent": "تم الإرسال",
|
||||
"roomRedPacketHistoryStatusFinished": "تم استلامها بالكامل",
|
||||
"roomRedPacketHistoryStatusPending": "قيد الانتظار",
|
||||
"roomRedPacketHistoryStatusReturned": "تم الإرجاع",
|
||||
"roomRedPacketHistoryRefund": "المسترد: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "سجل الاستلام",
|
||||
"roomRedPacketChatMessage": "أرسلت لك مظروفاً أحمر، افتحه الآن!",
|
||||
"roomRedPacketReceivedMessage": "استلم {1} عملة من حزمة الحظ {2}",
|
||||
"roomRedPacketOpenSubtitle": "وصلت حزمة الحظ",
|
||||
"roomRedPacketOpenYouGot": "حصلت على",
|
||||
"roomRedPacketClaimSuccessTips": "تم إرسال العملات إلى محفظتك.",
|
||||
"roomRedPacketOpenSoldOut": "تم استلام حزمة الحظ بالكامل.",
|
||||
"roomRedPacketOpenExpired": "انتهت صلاحية حزمة الحظ.",
|
||||
"roomRedPacketRegionNotMatch": "يمكن فقط للمستخدمين في نفس منطقة اللغة استلام حزمة الحظ.",
|
||||
"roomRedPacketNotStarted": "حزمة الحظ غير جاهزة بعد.",
|
||||
"roomRedPacketDailyLimitExceeded": "تم الوصول إلى حد الإرسال اليومي.",
|
||||
"roomRedPacketInsufficientBalance": "العملات الذهبية غير كافية.",
|
||||
"roomRedPacketPresenceRequired": "يرجى البقاء داخل الغرفة لاستخدام حزم الحظ.",
|
||||
"roomRedPacketNetworkError": "تعذر معالجة حزمة الحظ الآن.",
|
||||
"roomRedPacketAlreadyClaimed": "لقد استلمت حزمة الحظ هذه بالفعل.",
|
||||
"roomRedPacketOpenWaiting": "يمكن فتحها بعد {1} دقائق.",
|
||||
"roomRedPacketOpenWaitingCountdown": "متاح للاستلام بعد {1}",
|
||||
"roomRedPacketOkay": "حسناً",
|
||||
"roomRedPacketFloatingTitle": "{1} أرسل مظروفاً أحمر",
|
||||
"roomRedPacketFloatingSubtitle": "أرسل مظروفاً أحمر",
|
||||
"redEnvelopeTips1": "عملات:",
|
||||
"roomTools": "أدوات الغرفة:",
|
||||
"entertainment": "الترفيه:",
|
||||
"reportSucc": "تم الإبلاغ بنجاح",
|
||||
@ -540,14 +578,21 @@
|
||||
"resetsDailyAtMidnight": "يُعاد التعيين يوميًا عند منتصف الليل",
|
||||
"limitedToOneTime": "لمرة واحدة فقط",
|
||||
"useMicrophoneForOneMin": "استخدم الميكروفون لمدة دقيقة واحدة",
|
||||
"taskRequirement": "المتطلبات",
|
||||
"taskProgress": "التقدم",
|
||||
"rewardClaimedSuccessfully": "تم استلام المكافأة بنجاح",
|
||||
"rewardClaimFailed": "فشل استلام المكافأة",
|
||||
"nickName": "الاسم المستعار",
|
||||
"giftSpecialEffects": "تأثيرات الهدايا الخاصة",
|
||||
"country": "دولة",
|
||||
"basicFeatures": "الميزات الأساسية",
|
||||
"floatingAnimationInGlobal": "الرسوم المتحركة العائمة في العالم",
|
||||
"joinMemberTips": "إذا كنت زائرًا في الغرفة، لا يمكنك أخذ الميكروفون.",
|
||||
"country": "دولة",
|
||||
"basicFeatures": "الميزات الأساسية",
|
||||
"floatingAnimationInGlobal": "الرسوم المتحركة العائمة في العالم",
|
||||
"broadcast": "البث",
|
||||
"broadcastDisplay": "عرض البث",
|
||||
"broadcastAllPlaces": "كل الأماكن",
|
||||
"broadcastRoomOnly": "داخل الغرفة",
|
||||
"broadcastOff": "إغلاق البث",
|
||||
"joinMemberTips": "إذا كنت زائرًا في الغرفة، لا يمكنك أخذ الميكروفون.",
|
||||
"countryRegion": "البلد والمنطقة",
|
||||
"gender": "الجنس",
|
||||
"likedYourComment": "أعجب بتعليقك.",
|
||||
|
||||
@ -177,6 +177,44 @@
|
||||
"sendRedPackConfirmTips": "আপনি কি লাল প্যাক পাঠাতে চান?",
|
||||
"redEnvelopeSendingRecords": "লাল খাম প্রেরণ রেকর্ড:",
|
||||
"redEnvelope": "লাল খাম",
|
||||
"roomRedPacketGoldCoins": "স্বর্ণ কয়েন",
|
||||
"roomRedPacketQuantity": "সংখ্যা",
|
||||
"roomRedPacketPickupTime": "গ্রহণের সময়",
|
||||
"roomRedPacketInMinutes": "{1} মিনিট পরে",
|
||||
"roomRedPacketClaimNow": "এখন গ্রহণ",
|
||||
"roomRedPacketSendLuckyPack": "লাকি প্যাক পাঠান",
|
||||
"roomRedPacketSending": "পাঠানো হচ্ছে...",
|
||||
"roomRedPacketSendSuccess": "লাকি প্যাক পাঠানো হয়েছে",
|
||||
"roomRedPacketComingSoon": "শীঘ্রই আসছে",
|
||||
"roomRedPacketRulesTitle": "নিয়ম",
|
||||
"roomRedPacketRulesContent": "1. আপনি কয়েন খরচ করে লাকি প্যাক পাঠাতে পারেন। অন্যরা আপনার লাকি প্যাক খুললে তারা এলোমেলো পরিমাণ কয়েন পেতে পারে।\n\n2. লাকি প্যাক পাঠানোর ২ ঘণ্টা পরে, যে কয়েনগুলো কেউ গ্রহণ করেনি সেগুলো আপনার ওয়ালেটে ফেরত যাবে।\n\n3. সার্ভার-ব্যাপী লাকি প্যাক পাঠানোর পর সার্ভার-ব্যাপী লাকি প্যাক ব্যানার দেখানো হবে, ফলে আপনি আরও বেশি মনোযোগ পাবেন।\n\n4. আপনি প্রতিদিন %1$sটি রুম লাকি প্যাক পাঠাতে পারবেন।\n\n5. একই অঞ্চলের নয় এমন ব্যবহারকারীদের পাঠানো লাকি প্যাক গ্রহণ করা যাবে না।",
|
||||
"roomRedPacketHistoryTitle": "লাকি প্যাক পাঠানোর ইতিহাস",
|
||||
"roomRedPacketHistoryItemTitle": "লাকি প্যাক ({1} খাম)",
|
||||
"roomRedPacketHistoryStatusSent": "পাঠানো হয়েছে",
|
||||
"roomRedPacketHistoryStatusFinished": "সব গ্রহণ করা হয়েছে",
|
||||
"roomRedPacketHistoryStatusPending": "অপেক্ষায়",
|
||||
"roomRedPacketHistoryStatusReturned": "ফেরত দেওয়া হয়েছে",
|
||||
"roomRedPacketHistoryRefund": "ফেরত: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "গ্রহণের রেকর্ড",
|
||||
"roomRedPacketChatMessage": "আমি তোমাকে একটি লাল খাম পাঠিয়েছি, এখনই খোলো!",
|
||||
"roomRedPacketReceivedMessage": "{2} লাকি প্যাক থেকে {1} কয়েন গ্রহণ করুন",
|
||||
"roomRedPacketOpenSubtitle": "লাকি প্যাক এসেছে",
|
||||
"roomRedPacketOpenYouGot": "তুমি পেয়েছ",
|
||||
"roomRedPacketClaimSuccessTips": "কয়েন তোমার ওয়ালেটে পাঠানো হয়েছে।",
|
||||
"roomRedPacketOpenSoldOut": "লাকি প্যাক ইতিমধ্যে শেষ হয়েছে।",
|
||||
"roomRedPacketOpenExpired": "লাকি প্যাকের সময় শেষ হয়েছে।",
|
||||
"roomRedPacketRegionNotMatch": "শুধু একই ভাষা অঞ্চলের ব্যবহারকারীরা এই লাকি প্যাক গ্রহণ করতে পারবেন।",
|
||||
"roomRedPacketNotStarted": "লাকি প্যাক এখনো প্রস্তুত নয়।",
|
||||
"roomRedPacketDailyLimitExceeded": "আজকের পাঠানোর সীমা শেষ হয়েছে।",
|
||||
"roomRedPacketInsufficientBalance": "স্বর্ণ কয়েন যথেষ্ট নয়।",
|
||||
"roomRedPacketPresenceRequired": "লাকি প্যাক ব্যবহার করতে রুমে থাকুন।",
|
||||
"roomRedPacketNetworkError": "এখন লাকি প্যাক প্রক্রিয়া করা যাচ্ছে না।",
|
||||
"roomRedPacketAlreadyClaimed": "তুমি ইতিমধ্যে এই লাকি প্যাক গ্রহণ করেছ।",
|
||||
"roomRedPacketOpenWaiting": "{1} মিনিট পরে খোলা যাবে।",
|
||||
"roomRedPacketOpenWaitingCountdown": "{1} পরে গ্রহণ করা যাবে",
|
||||
"roomRedPacketOkay": "ঠিক আছে",
|
||||
"roomRedPacketFloatingTitle": "{1} একটি লাল খাম পাঠিয়েছে",
|
||||
"roomRedPacketFloatingSubtitle": "একটি লাল খাম পাঠিয়েছে",
|
||||
"redEnvelopeRecTips2": "সব লাল খাম দাবি করা হয়েছে।",
|
||||
"redEnvelopeRecTips3": "লাল খাম সংগ্রহের সময় শেষ হয়ে গেছে!",
|
||||
"openTheTreasureChest": "সম্পদ বাক্স খুলুন",
|
||||
@ -465,12 +503,19 @@
|
||||
"importantReminder": "গুরুত্বপূর্ণ স্মারক",
|
||||
"entryVehicleAnimation": "প্রবেশ যান অ্যানিমেশন",
|
||||
"floatingAnimationInGlobal": "গ্লোবালে ভাসমান অ্যানিমেশন",
|
||||
"broadcast": "ব্রডকাস্ট",
|
||||
"broadcastDisplay": "ব্রডকাস্ট দেখানো",
|
||||
"broadcastAllPlaces": "সব জায়গায়",
|
||||
"broadcastRoomOnly": "রুমে",
|
||||
"broadcastOff": "ব্রডকাস্ট বন্ধ",
|
||||
"entryVehicleAnimation2": "VIP4 বা তার বেশি অধিকারধারী ব্যবহারকারীরা এই ফিচার ব্যবহার করে গাড়ির অ্যানিমেশন বন্ধ করতে পারেন।",
|
||||
"dailyTasks": "দৈনন্দিন টাস্ক",
|
||||
"exclusiveForNewcomers": "নতুনদের জন্য বিশেষ",
|
||||
"resetsDailyAtMidnight": "প্রতিদিন মধ্যরাতে রিসেট হয়",
|
||||
"limitedToOneTime": "শুধু একবারের জন্য",
|
||||
"useMicrophoneForOneMin": "১ মিনিট মাইক্রোফোন ব্যবহার করুন",
|
||||
"taskRequirement": "প্রয়োজন",
|
||||
"taskProgress": "অগ্রগতি",
|
||||
"rewardClaimedSuccessfully": "পুরস্কার সফলভাবে গ্রহণ করা হয়েছে",
|
||||
"rewardClaimFailed": "পুরস্কার গ্রহণ ব্যর্থ হয়েছে",
|
||||
"enterRoomConfirmTips": "আপনি কি রুমে যেতে চান?",
|
||||
|
||||
@ -148,6 +148,44 @@
|
||||
"sendRedPackConfirmTips": "Are you sure you want to send the red packet?",
|
||||
"redEnvelopeSendingRecords": "Red envelope sending records:",
|
||||
"redEnvelope": "Red Envelope",
|
||||
"roomRedPacketGoldCoins": "Gold Coins",
|
||||
"roomRedPacketQuantity": "Quantity",
|
||||
"roomRedPacketPickupTime": "Pickup Time",
|
||||
"roomRedPacketInMinutes": "In {1} Minutes",
|
||||
"roomRedPacketClaimNow": "Claim Now",
|
||||
"roomRedPacketSendLuckyPack": "Send A Lucky Pack",
|
||||
"roomRedPacketSending": "Sending...",
|
||||
"roomRedPacketSendSuccess": "Lucky pack sent",
|
||||
"roomRedPacketComingSoon": "Coming soon",
|
||||
"roomRedPacketRulesTitle": "Rules",
|
||||
"roomRedPacketRulesContent": "1. You can spend coins to send a Lucky Pack. If others open your Lucky Pack, they can receive a random amount of coins.\n\n2. Two hours after a Lucky Pack is sent, any unclaimed coins will be returned to your wallet.\n\n3. After you send a server-wide Lucky Pack, a server-wide Lucky Pack banner will be triggered, bringing you more attention.\n\n4. You can send %1$s room Lucky Packs per day.\n\n5. Lucky Packs sent by users outside your region cannot be claimed.",
|
||||
"roomRedPacketHistoryTitle": "Lucky Pack Delivery History",
|
||||
"roomRedPacketHistoryItemTitle": "Lucky Bag ({1} red envelopes)",
|
||||
"roomRedPacketHistoryStatusSent": "Sent",
|
||||
"roomRedPacketHistoryStatusFinished": "Fully claimed",
|
||||
"roomRedPacketHistoryStatusPending": "Pending",
|
||||
"roomRedPacketHistoryStatusReturned": "Returned",
|
||||
"roomRedPacketHistoryRefund": "Refund: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "Claim Records",
|
||||
"roomRedPacketChatMessage": "I sent you a red envelope, open it right away!",
|
||||
"roomRedPacketReceivedMessage": "Claim {1} coins from the {2} Lucky Pack",
|
||||
"roomRedPacketOpenSubtitle": "The Lucky Pack Has Arrived",
|
||||
"roomRedPacketOpenYouGot": "You Get",
|
||||
"roomRedPacketClaimSuccessTips": "Coins have been sent to your wallet.",
|
||||
"roomRedPacketOpenSoldOut": "The Lucky Pack Has Already Sold Out.",
|
||||
"roomRedPacketOpenExpired": "The Lucky Pack Has Expired.",
|
||||
"roomRedPacketRegionNotMatch": "Only users in the same language region can claim this lucky pack.",
|
||||
"roomRedPacketNotStarted": "The lucky pack is not ready yet.",
|
||||
"roomRedPacketDailyLimitExceeded": "Today's sending limit has been reached.",
|
||||
"roomRedPacketInsufficientBalance": "Insufficient gold coins.",
|
||||
"roomRedPacketPresenceRequired": "Please stay in the room to use lucky packs.",
|
||||
"roomRedPacketNetworkError": "Unable to process the lucky pack right now.",
|
||||
"roomRedPacketAlreadyClaimed": "You have already claimed this lucky pack.",
|
||||
"roomRedPacketOpenWaiting": "Can be opened in {1} minutes.",
|
||||
"roomRedPacketOpenWaitingCountdown": "Available For Pickup After {1}",
|
||||
"roomRedPacketOkay": "Okay",
|
||||
"roomRedPacketFloatingTitle": "{1} sent a red envelope",
|
||||
"roomRedPacketFloatingSubtitle": "Sent a red envelope",
|
||||
"redEnvelopeRecTips2": "The red envelopes have all been claimed.",
|
||||
"redEnvelopeRecTips3": "The red envelope collection time has expired!",
|
||||
"openTheTreasureChest": "Open the treasure chest",
|
||||
@ -435,12 +473,19 @@
|
||||
"importantReminder": "Important Reminder",
|
||||
"entryVehicleAnimation": "Entry vehicle animation",
|
||||
"floatingAnimationInGlobal": "Floating animation in global",
|
||||
"broadcast": "Broadcast",
|
||||
"broadcastDisplay": "Broadcast display",
|
||||
"broadcastAllPlaces": "All places",
|
||||
"broadcastRoomOnly": "In room",
|
||||
"broadcastOff": "Close broadcast",
|
||||
"entryVehicleAnimation2": "Users with VIP4 or higher privileges can use this function to disable vehicle animations.",
|
||||
"dailyTasks": "Daily Tasks",
|
||||
"exclusiveForNewcomers": "Exclusive For Newcomers",
|
||||
"resetsDailyAtMidnight": "Resets daily at midnight",
|
||||
"limitedToOneTime": "Limited to one time",
|
||||
"useMicrophoneForOneMin": "Use the microphone for 1 min",
|
||||
"taskRequirement": "Requirement",
|
||||
"taskProgress": "Progress",
|
||||
"rewardClaimedSuccessfully": "Reward claimed successfully",
|
||||
"rewardClaimFailed": "Reward claim failed",
|
||||
"enterRoomConfirmTips": "Are you sure you want to enter the room?",
|
||||
|
||||
@ -135,9 +135,47 @@
|
||||
"collectionTimeTips": "Toplama Zamanı:{1}({2}/{3})",
|
||||
"sendARedEnvelope": "Kırmızı Zarf Gönder",
|
||||
"sendRedPackConfirmTips": "Kırmızı zarfı göndermek istediğinizden emin misiniz?",
|
||||
"redEnvelopeSendingRecords": "Kırmızı zarf gönderim kayıtları:",
|
||||
"redEnvelope": "Kırmızı Zarf",
|
||||
"redEnvelopeRecTips2": "Kırmızı zarfların hepsi talep edildi.",
|
||||
"redEnvelopeSendingRecords": "Kırmızı zarf gönderim kayıtları:",
|
||||
"redEnvelope": "Kırmızı Zarf",
|
||||
"roomRedPacketGoldCoins": "Altın Jeton",
|
||||
"roomRedPacketQuantity": "Adet",
|
||||
"roomRedPacketPickupTime": "Alma Zamanı",
|
||||
"roomRedPacketInMinutes": "{1} dk sonra",
|
||||
"roomRedPacketClaimNow": "Şimdi Al",
|
||||
"roomRedPacketSendLuckyPack": "Şans Paketi Gönder",
|
||||
"roomRedPacketSending": "Gönderiliyor...",
|
||||
"roomRedPacketSendSuccess": "Şans paketi gönderildi",
|
||||
"roomRedPacketComingSoon": "Yakında",
|
||||
"roomRedPacketRulesTitle": "Kurallar",
|
||||
"roomRedPacketRulesContent": "1. Jeton harcayarak Şans Paketi gönderebilirsin. Başkaları Şans Paketini açarsa rastgele miktarda jeton kazanabilir.\n\n2. Şans Paketi gönderildikten 2 saat sonra alınmayan jetonlar cüzdanına iade edilir.\n\n3. Tüm sunucuya Şans Paketi gönderdikten sonra tüm sunucu Şans Paketi bannerı tetiklenir ve daha fazla dikkat çekersin.\n\n4. Günde %1$s oda Şans Paketi gönderebilirsin.\n\n5. Aynı bölgeden olmayan kullanıcıların gönderdiği Şans Paketleri alınamaz.",
|
||||
"roomRedPacketHistoryTitle": "Şans Paketi Gönderim Geçmişi",
|
||||
"roomRedPacketHistoryItemTitle": "Şans Paketi ({1} zarf)",
|
||||
"roomRedPacketHistoryStatusSent": "Gönderildi",
|
||||
"roomRedPacketHistoryStatusFinished": "Tamamı alındı",
|
||||
"roomRedPacketHistoryStatusPending": "Beklemede",
|
||||
"roomRedPacketHistoryStatusReturned": "İade edildi",
|
||||
"roomRedPacketHistoryRefund": "İade: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "Alma Kayıtları",
|
||||
"roomRedPacketChatMessage": "Sana bir kırmızı zarf gönderdim, hemen aç!",
|
||||
"roomRedPacketReceivedMessage": "{2} Şans Paketinden {1} jeton al",
|
||||
"roomRedPacketOpenSubtitle": "Şans Paketi Geldi",
|
||||
"roomRedPacketOpenYouGot": "Kazandın",
|
||||
"roomRedPacketClaimSuccessTips": "Jetonlar cüzdanına gönderildi.",
|
||||
"roomRedPacketOpenSoldOut": "Şans Paketi tamamen alındı.",
|
||||
"roomRedPacketOpenExpired": "Şans Paketinin süresi doldu.",
|
||||
"roomRedPacketRegionNotMatch": "Bu şans paketini yalnızca aynı dil bölgesindeki kullanıcılar alabilir.",
|
||||
"roomRedPacketNotStarted": "Şans paketi henüz hazır değil.",
|
||||
"roomRedPacketDailyLimitExceeded": "Bugünkü gönderim sınırına ulaşıldı.",
|
||||
"roomRedPacketInsufficientBalance": "Altın jeton yetersiz.",
|
||||
"roomRedPacketPresenceRequired": "Şans paketlerini kullanmak için odada kalın.",
|
||||
"roomRedPacketNetworkError": "Şans paketi şu anda işlenemiyor.",
|
||||
"roomRedPacketAlreadyClaimed": "Bu şans paketini zaten aldın.",
|
||||
"roomRedPacketOpenWaiting": "{1} dakika sonra açılabilir.",
|
||||
"roomRedPacketOpenWaitingCountdown": "{1} sonra alınabilir",
|
||||
"roomRedPacketOkay": "Tamam",
|
||||
"roomRedPacketFloatingTitle": "{1} kırmızı zarf gönderdi",
|
||||
"roomRedPacketFloatingSubtitle": "Kırmızı zarf gönderdi",
|
||||
"redEnvelopeRecTips2": "Kırmızı zarfların hepsi talep edildi.",
|
||||
"redEnvelopeRecTips3": "Kırmızı zarf toplama zamanı doldu!",
|
||||
"openTheTreasureChest": "Hazine Sandığını Aç",
|
||||
"redEnvelopeRecTips1": "Kazanan jettonlar cüzdanınıza yatırıldı.",
|
||||
@ -431,15 +469,22 @@
|
||||
"giftSpecialEffects": "Hediye Özel Efektleri",
|
||||
"basicFeatures": "Temel Özellikler",
|
||||
"task": "Görev",
|
||||
"importantReminder": "Önemli Hatırlatma",
|
||||
"entryVehicleAnimation": "Giriş Aracı Animasyonu",
|
||||
"importantReminder": "Önemli Hatırlatma",
|
||||
"entryVehicleAnimation": "Giriş Aracı Animasyonu",
|
||||
"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.",
|
||||
"broadcast": "Yayın",
|
||||
"broadcastDisplay": "Yayın gösterimi",
|
||||
"broadcastAllPlaces": "Her yerde",
|
||||
"broadcastRoomOnly": "Odada",
|
||||
"broadcastOff": "Yayını kapat",
|
||||
"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",
|
||||
"exclusiveForNewcomers": "Yeni Gelenlere Özel",
|
||||
"resetsDailyAtMidnight": "Her gün gece yarısı sıfırlanır",
|
||||
"limitedToOneTime": "Yalnızca bir kez",
|
||||
"useMicrophoneForOneMin": "Mikrofonu 1 dakika kullan",
|
||||
"taskRequirement": "Gereksinim",
|
||||
"taskProgress": "İlerleme",
|
||||
"rewardClaimedSuccessfully": "Ödül başarıyla alındı",
|
||||
"rewardClaimFailed": "Ödül alınamadı",
|
||||
"enterRoomConfirmTips": "Odaya girmek istediğinizden emin misiniz?",
|
||||
|
||||
378
docs/voice-room-big-gift-global-floating-plan.md
Normal file
378
docs/voice-room-big-gift-global-floating-plan.md
Normal file
@ -0,0 +1,378 @@
|
||||
# 语音房大额礼物全局飘窗方案
|
||||
|
||||
整理时间:2026-05-06
|
||||
适用范围:语音房普通大额礼物飘窗;只在一级页面 `home / explore / message / me` 展示;点击进入对应房间。
|
||||
本轮只做方案整理,不改业务代码。
|
||||
|
||||
## 1. 需求目标
|
||||
|
||||
在用户送出大额礼物时,新增一个顶部飘窗,展示类似:
|
||||
|
||||
> 用户 A 在房间 B 送了 C 礼物
|
||||
|
||||
要求:
|
||||
|
||||
- 礼物类型:普通礼物大额送礼,明确排除幸运礼物、魔法礼物等走 `giveLuckyGift` 的礼物。
|
||||
- 展示范围:全局一级页面,但只限首页主框架内的四个 Tab:`home / explore / message / me`。
|
||||
- 不在语音房页面、二级页面、设置页、充值页、聊天详情页等页面展示。
|
||||
- 点击飘窗后进入具体房间。
|
||||
- 飘窗位置沿用当前幸运礼物/礼物飘屏顶部位置。
|
||||
|
||||
## 2. 当前代码现状
|
||||
|
||||
### 2.1 顶部飘窗能力已存在
|
||||
|
||||
现有飘窗由 `OverlayManager` 统一调度:
|
||||
|
||||
- 文件:`lib/shared/data_sources/sources/local/floating_screen_manager.dart`
|
||||
- 入口:`OverlayManager().addMessage(SCFloatingMessage)`
|
||||
- 队列:`SCPriorityQueue<SCFloatingMessage>`,按 `priority` 排序。
|
||||
- 位置:`OverlayEntry` 使用 `Align(topStart)`,再 `Transform.translate(offset: Offset(0, 70.w))`。
|
||||
- 动画组件:
|
||||
- `type = 0`:`FloatingLuckGiftScreenWidget`
|
||||
- `type = 1`:`FloatingGiftScreenWidget`
|
||||
- `type = 2`:游戏中奖
|
||||
- `type = 3`:火箭
|
||||
- `type = 4`:红包
|
||||
- `type = 5`:VIP 进房
|
||||
|
||||
现有 `FloatingLuckGiftScreenWidget` 和 `FloatingGiftScreenWidget` 都已经支持:
|
||||
|
||||
- 从右往左飘过。
|
||||
- 左滑快速关闭。
|
||||
- 点击后调用 `SCRoomUtils.goRoom(roomId, context, fromFloting: true)` 进入房间。
|
||||
|
||||
### 2.2 现有普通礼物飘窗是房间内逻辑
|
||||
|
||||
普通礼物发送成功后会进入 `GiftPage.sendGiftMsg(...)`:
|
||||
|
||||
- 文件:`lib/modules/gift/gift_page.dart`
|
||||
- 当前判断:`gift.giftCandy * quantity > 9999` 时创建 `SCFloatingMessage(type: 1)`。
|
||||
- 当前内容:用户 A 送给用户 B 礼物,字段偏向 `toUserName / toUserAvatarUrl`。
|
||||
- 当前触发范围:发送者本地和当前房间 RTM 收到 `SCRoomMsgType.gift` 后触发。
|
||||
|
||||
这条逻辑不适合直接作为“全局大额礼物飘窗”:
|
||||
|
||||
- 它是房间消息,不是全服消息。
|
||||
- 它按收礼人循环,可能一次多目标送礼触发多条。
|
||||
- UI 文案是“send to 某用户”,不是“在房间 B 送了 C 礼物”。
|
||||
- `OverlayManager._shouldDisplayMessage(...)` 当前对 `type = 0 / 1 / 5` 要求必须匹配当前房间 ID,否则不展示。
|
||||
|
||||
### 2.3 全服广播通道已存在
|
||||
|
||||
RTM 初始化后会加入全服广播群:
|
||||
|
||||
- 文件:`lib/services/audio/rtm_manager.dart`
|
||||
- `init(...)` 中调用 `joinBigBroadcastGroup()`
|
||||
- 全服消息在 `_newBroadCastMsgRecv(...)` 处理
|
||||
- 当前已处理:
|
||||
- `GAME_LUCKY_GIFT`
|
||||
- 游戏中奖
|
||||
- 火箭
|
||||
- 红包
|
||||
|
||||
因此新增大额普通礼物全局飘窗时,最合适的入口是全服广播群,而不是房间群消息。
|
||||
|
||||
## 3. 推荐总体方案
|
||||
|
||||
推荐采用:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["用户在房间送普通礼物"] --> B["后端校验送礼成功"]
|
||||
B --> C{"是否普通大额礼物"}
|
||||
C -- "否" --> D["只走现有房间送礼消息"]
|
||||
C -- "是" --> E["后端推送 BIG_GIFT 到全服广播群"]
|
||||
E --> F["客户端 _newBroadCastMsgRecv 接收"]
|
||||
F --> G{"当前是否首页四个一级 Tab"}
|
||||
G -- "否" --> H["丢弃,不入队"]
|
||||
G -- "是" --> I["OverlayManager 入队展示顶部飘窗"]
|
||||
I --> J["点击 SCRoomUtils.goRoom(roomId)"]
|
||||
```
|
||||
|
||||
核心点:
|
||||
|
||||
- 大额判断放后端做,前端只负责展示,避免客户端伪造全服大礼物消息。
|
||||
- 前端新增独立消息类型,例如 `BIG_GIFT`,不复用幸运礼物 `GAME_LUCKY_GIFT`。
|
||||
- 前端新增独立飘窗类型,例如 `SCFloatingMessage.type = 6`,避免影响当前房间内 `type = 1` 的普通礼物飘窗。
|
||||
- 展示作用域由 `OverlayManager` 或主框架路由状态控制,只允许首页四 Tab 展示。
|
||||
|
||||
## 4. 服务端数据建议
|
||||
|
||||
建议后端在普通礼物接口送礼成功后判断是否需要推送全服广播:
|
||||
|
||||
- 接口链路:`/gift/batch`,前端对应 `SCChatRoomRepository.giveGift(...)`。
|
||||
- 排除链路:`/gift/give/lucky-gift`,前端对应 `giveLuckyGift(...)`。
|
||||
- 推荐阈值:沿用客户端现有房间内逻辑 `totalCoins >= 10000`,最终以产品/后端配置为准。
|
||||
- 推荐只推送一次送礼行为,不按收礼人数量重复推送。
|
||||
|
||||
推荐全服广播 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "BIG_GIFT",
|
||||
"data": {
|
||||
"eventId": "giftSendRecordId or unique id",
|
||||
"roomId": "room id",
|
||||
"roomName": "room name",
|
||||
"sendUserId": "sender user id",
|
||||
"sendUserName": "sender nickname",
|
||||
"sendUserAvatar": "sender avatar",
|
||||
"giftId": "gift id",
|
||||
"giftName": "gift name",
|
||||
"giftPhoto": "gift cover url",
|
||||
"giftQuantity": 1,
|
||||
"giftCandy": 10000,
|
||||
"totalCoins": 10000,
|
||||
"timestamp": 1710000000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `eventId`:用于客户端去重。没有则用 `roomId + sendUserId + giftId + giftQuantity + timestamp` 做兜底 key。
|
||||
- `roomId`:点击飘窗进房必须字段。
|
||||
- `roomName`:满足“在房间 B”的展示需求。
|
||||
- `giftName / giftPhoto`:满足“送了 C 礼物”的展示和礼物图标展示。
|
||||
- `totalCoins`:可用于优先级、样式分级或埋点。
|
||||
|
||||
## 5. 前端改造点
|
||||
|
||||
### 5.1 新增大额礼物广播模型
|
||||
|
||||
建议新增模型:
|
||||
|
||||
- `lib/shared/business_logic/models/res/sc_big_gift_broadcast_push.dart`
|
||||
|
||||
字段对应服务端 payload。不要把所有字段硬塞进 `SCBroadCastLuckGiftPush`,幸运礼物模型语义不同。
|
||||
|
||||
也可以扩展 `SCFloatingMessage`:
|
||||
|
||||
- 新增 `roomName`
|
||||
- 新增 `giftName`
|
||||
- 新增 `eventId`
|
||||
- 保留 `roomId / userName / userAvatarUrl / giftUrl / giftId / number / coins / priority`
|
||||
|
||||
### 5.2 RTM 全服广播接入
|
||||
|
||||
在 `RealTimeMessagingManager._newBroadCastMsgRecv(...)` 增加:
|
||||
|
||||
```dart
|
||||
} else if (type == "BIG_GIFT") {
|
||||
final push = SCBigGiftBroadcastPush.fromJson(data);
|
||||
_handleBigGiftGlobalNews(push);
|
||||
}
|
||||
```
|
||||
|
||||
`_handleBigGiftGlobalNews(...)` 推荐职责:
|
||||
|
||||
- 校验 `roomId`、`sendUserName`、`giftName` 至少有可展示值。
|
||||
- 排除幸运礼物或服务端标记的非普通礼物。
|
||||
- 去重。
|
||||
- 组装 `SCFloatingMessage(type: 6, priority: 900, ...)`。
|
||||
- 调用 `OverlayManager().addMessage(...)`。
|
||||
|
||||
优先级建议:
|
||||
|
||||
- 幸运礼物:当前 `priority = 1000`。
|
||||
- 大额普通礼物:建议 `priority = 900`。
|
||||
- 普通房间礼物本地飘窗:保持默认或现有逻辑。
|
||||
|
||||
### 5.3 新增大额礼物飘窗 Widget
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ui_kit/widgets/room/floating/floating_big_gift_screen_widget.dart`
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 `FloatingGiftScreenWidget` 展示的是“用户 A sendTo 用户 B + 礼物图标 x 数量”。
|
||||
- 新需求是“用户 A 在房间 B 送了 C 礼物”。
|
||||
- 新增 Widget 可以复用现有动画、点击、左滑关闭逻辑,但不影响房间内普通礼物飘窗。
|
||||
|
||||
UI 建议:
|
||||
|
||||
- 保持同一位置和相近尺寸:`height 54.w` 左右,`width 320.w - 350.w`。
|
||||
- 左侧:送礼用户头像和昵称。
|
||||
- 中间:房间名和文案,例如 `在 {roomName} 送出`。
|
||||
- 右侧:礼物图标 + 礼物名,可保留 `x{quantity}`。
|
||||
- 长昵称、长房间名、长礼物名使用 `TextOverflow.ellipsis` 或局部跑马灯,避免遮挡。
|
||||
- RTL 语言需要检查布局方向,至少保持点击和左滑逻辑与现有组件一致。
|
||||
|
||||
`OverlayManager._buildScreenWidget(...)` 增加:
|
||||
|
||||
```dart
|
||||
case 6:
|
||||
return FloatingBigGiftScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
```
|
||||
|
||||
### 5.4 展示作用域:只允许首页四 Tab
|
||||
|
||||
当前 `OverlayManager` 是根 Overlay 展示,天然能覆盖全 App。需要新增“允许展示作用域”,否则可能在二级页、房间页也展示。
|
||||
|
||||
推荐方案:由 `SCIndexPage` 作为主框架控制作用域。
|
||||
|
||||
实现思路:
|
||||
|
||||
- `SCIndexPage` 接入 `RouteAware`,订阅全局 `routeObserver`。
|
||||
- 当 `SCIndexPage` 是栈顶时,设置 `OverlayManager().setMainTabsVisible(true)`。
|
||||
- 当从首页 push 到任意二级页面时,`didPushNext` 设置为 false。
|
||||
- 当从二级页返回首页时,`didPopNext` 设置为 true。
|
||||
- `SCIndexPage.dispose` 设置为 false。
|
||||
- `OverlayManager.addMessage(...)` 或 `_shouldDisplayMessage(...)` 对 `type = 6` 判断 `mainTabsVisible == true`,否则直接丢弃或不入队。
|
||||
|
||||
不推荐只用 `navigator.canPop()`:
|
||||
|
||||
- 可以作为兜底,但语义不够清晰。
|
||||
- 登录页、启动页也可能是根页面,不能等同于首页四 Tab。
|
||||
|
||||
### 5.5 调整现有展示过滤
|
||||
|
||||
当前 `OverlayManager._shouldDisplayMessage(...)` 对 `type = 0 / 1 / 5` 做当前房间匹配:
|
||||
|
||||
```dart
|
||||
if (message.type != 0 && message.type != 1 && message.type != 5) {
|
||||
return true;
|
||||
}
|
||||
...
|
||||
return currentRoomId == messageRoomId;
|
||||
```
|
||||
|
||||
新增 `type = 6` 时不要走当前房间匹配,而是走首页四 Tab 匹配:
|
||||
|
||||
- `type = 6`:只判断首页四 Tab 是否可见、全局飘屏开关是否开启、必要字段是否完整。
|
||||
- `type = 0 / 1 / 5`:保留现有房间匹配,避免破坏当前房间内特效体验。
|
||||
- `type = 2 / 3 / 4`:按现有逻辑评估是否也需要作用域限制,本需求不建议顺手改。
|
||||
|
||||
### 5.6 点击进房
|
||||
|
||||
新 Widget 点击继续复用:
|
||||
|
||||
```dart
|
||||
SCRoomUtils.goRoom(
|
||||
message.roomId!,
|
||||
navigatorKey.currentState!.context,
|
||||
fromFloting: true,
|
||||
);
|
||||
```
|
||||
|
||||
现有 `SCRoomUtils.goRoom(...)` 已处理:
|
||||
|
||||
- 当前没有房间:弹确认后进房。
|
||||
- 当前房间最小化且房间相同:打开当前房间。
|
||||
- 当前已在其他房间:弹确认切房。
|
||||
|
||||
这里不需要新增路由能力。
|
||||
|
||||
## 6. 客户端兜底方案
|
||||
|
||||
如果后端暂时不能推送 `BIG_GIFT`,可以做临时客户端兜底,但不推荐作为长期方案。
|
||||
|
||||
临时做法:
|
||||
|
||||
- 在 `GiftPage._executeGiftRequest(...)` 普通礼物成功后判断:
|
||||
- `!request.isLuckyGiftRequest`
|
||||
- `giftTab` 不是 `LUCK / LUCKY_GIFT / MAGIC`
|
||||
- `gift.giftCandy * request.quantity >= threshold`
|
||||
- 构造 `BigBroadcastGroupMessage("BIG_GIFT", SCFloatingMessage(...))`。
|
||||
- 调用 `RtmProvider.sendBigBroadcastGroup(...)`。
|
||||
- 必须只按一次送礼行为发送,不能放在 `sendGiftMsg(...)` 的 `acceptUsers` 循环里。
|
||||
|
||||
风险:
|
||||
|
||||
- 客户端可伪造全服广播,不够可信。
|
||||
- 多端并发、弱网重试可能重复。
|
||||
- 阈值和礼物类型判断容易与后端不一致。
|
||||
|
||||
因此正式方案仍建议后端推送。
|
||||
|
||||
## 7. 去重与队列策略
|
||||
|
||||
建议新增去重缓存:
|
||||
|
||||
- 位置:`RealTimeMessagingManager` 或 `OverlayManager`。
|
||||
- 结构:`LinkedHashMap<String, int>`,保存最近 100 条 event key 和时间。
|
||||
- TTL:30 秒到 60 秒。
|
||||
- key 优先使用 `eventId`。
|
||||
|
||||
队列策略:
|
||||
|
||||
- 继续使用 `OverlayManager` 现有优先级队列。
|
||||
- 大额礼物 `priority = 900`,低于幸运礼物大奖 `1000`。
|
||||
- 可选:当队列过长时丢弃低优先级大额礼物,避免首页连续刷屏。
|
||||
|
||||
## 8. 开关策略
|
||||
|
||||
沿用现有全局飘屏开关:
|
||||
|
||||
- `SCGlobalConfig.isFloatingAnimationInGlobal`
|
||||
- 本地持久化 key:`FloatingAnimationInGlobal`
|
||||
- 低性能设备会通过 `clampVisualEffectPreference(...)` 关闭。
|
||||
|
||||
建议:
|
||||
|
||||
- 大额礼物全局飘窗受这个开关控制。
|
||||
- 不受 `isGiftSpecialEffects` 控制,因为它是全屏礼物特效开关。
|
||||
- 不受 `isLuckGiftSpecialEffects` 控制,因为本需求不是幸运礼物。
|
||||
|
||||
## 9. 验收用例
|
||||
|
||||
### 9.1 基础展示
|
||||
|
||||
- 用户 A 在房间 B 送普通礼物 C,总价值达到阈值。
|
||||
- 当前用户在 `home`:看到顶部飘窗。
|
||||
- 当前用户在 `explore`:看到顶部飘窗。
|
||||
- 当前用户在 `message`:看到顶部飘窗。
|
||||
- 当前用户在 `me`:看到顶部飘窗。
|
||||
|
||||
### 9.2 范围限制
|
||||
|
||||
- 当前用户在语音房页面:不展示该全局大额礼物飘窗。
|
||||
- 当前用户在个人资料页、设置页、聊天详情页、充值页:不展示。
|
||||
- App 在启动页或登录页:不展示。
|
||||
|
||||
### 9.3 礼物类型
|
||||
|
||||
- 普通礼物达到阈值:展示。
|
||||
- 普通礼物未达到阈值:不展示。
|
||||
- 幸运礼物或魔法礼物达到金额:不走 `BIG_GIFT`,继续走现有幸运礼物逻辑。
|
||||
- 多人收礼的一次送礼:只展示一条。
|
||||
|
||||
### 9.4 点击进房
|
||||
|
||||
- 当前无房间:点击飘窗,确认后进入房间 B。
|
||||
- 当前房间已最小化且是房间 B:点击后打开当前房间。
|
||||
- 当前已在其他房间:点击后弹切房确认。
|
||||
- `roomId` 为空或房间不存在:不展示或进房失败时走现有错误提示。
|
||||
|
||||
### 9.5 稳定性
|
||||
|
||||
- 连续多条大额礼物:按队列顺序展示,不重叠。
|
||||
- 同一 `eventId` 重复到达:只展示一次。
|
||||
- 左滑关闭后下一条能继续展示。
|
||||
- 关闭全局飘屏开关后不展示。
|
||||
- 低性能设备默认不展示。
|
||||
|
||||
## 10. 建议实施顺序
|
||||
|
||||
1. 和后端确认 `BIG_GIFT` 全服广播协议、阈值、是否包含 `roomName / giftName / giftPhoto / eventId`。
|
||||
2. 新增 `SCBigGiftBroadcastPush` 模型和 `SCFloatingMessage` 扩展字段。
|
||||
3. 新增 `FloatingBigGiftScreenWidget`,复用现有动画、点击、左滑行为。
|
||||
4. `OverlayManager` 支持 `type = 6`,并新增首页四 Tab 可见性控制。
|
||||
5. `SCIndexPage` 用 `RouteAware` 控制 `OverlayManager` 的主 Tab 展示作用域。
|
||||
6. `RtmProvider._newBroadCastMsgRecv(...)` 接入 `BIG_GIFT`。
|
||||
7. 加去重和队列上限保护。
|
||||
8. 做手动验收与必要 Widget/单元测试。
|
||||
|
||||
## 11. 需要确认的问题
|
||||
|
||||
- 大额阈值最终是多少:沿用 `10000`,还是后端配置?
|
||||
- 飘窗是否展示收礼人:当前需求文案不需要,只写“用户 A 在房间 B 送了 C 礼物”。
|
||||
- 多目标送礼是否只展示一次:建议只展示一次。
|
||||
- 如果发送者自己也在首页四 Tab,是否也展示自己刚送的大额礼物:建议展示,和全服广播一致;如产品不希望自显,需要后端或前端按 `sendUserId == currentUserId` 过滤。
|
||||
- 房间 B 名称为空时的兜底文案:建议使用 `roomId` 或本地化的“语音房”。
|
||||
|
||||
@ -502,7 +502,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -511,7 +511,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -693,7 +693,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -702,7 +702,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -722,7 +722,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -731,7 +731,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -10,9 +10,28 @@ import UIKit
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
registerDurableAuthStorageChannel()
|
||||
registerMobileContextChannel()
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
private func registerMobileContextChannel() {
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
return
|
||||
}
|
||||
let channel = FlutterMethodChannel(
|
||||
name: "com.org.yumiparty/mobile_context",
|
||||
binaryMessenger: controller.binaryMessenger
|
||||
)
|
||||
channel.setMethodCallHandler { call, result in
|
||||
switch call.method {
|
||||
case "getTimeZoneIdentifier":
|
||||
result(TimeZone.current.identifier)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerDurableAuthStorageChannel() {
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
return
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/app/config/configs/sc_variant1_config.dart';
|
||||
import 'package:yumi/app/config/business_logic_strategy.dart';
|
||||
|
||||
@ -145,17 +144,9 @@ abstract class AppConfig {
|
||||
case 'variant1':
|
||||
default:
|
||||
_current = SCVariant1Config();
|
||||
debugPrint('AppConfig initialized for variant1 ');
|
||||
}
|
||||
|
||||
// 验证配置是否有效
|
||||
try {
|
||||
_current!.validate();
|
||||
} catch (e) {
|
||||
// validate方法在调试模式下不会抛出异常
|
||||
// 只有在发布模式下验证失败才会抛出异常
|
||||
debugPrint('应用配置验证失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
_current!.validate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ class SCVariant1Config implements AppConfig {
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名
|
||||
@ -29,10 +29,12 @@ class SCVariant1Config implements AppConfig {
|
||||
String get userAgreementUrl => 'https://h5.haiyihy.com/service.html'; // 正式用户协议页面
|
||||
|
||||
@override
|
||||
String get appDownloadUrlGoogle => 'https://play.google.com/store/apps/details?id=$packageName';
|
||||
String get appDownloadUrlGoogle =>
|
||||
'https://play.google.com/store/apps/details?id=$packageName';
|
||||
|
||||
@override
|
||||
String get appDownloadUrlApple => 'https://apps.apple.com/us/app/atuchat/id1234567890'; // 需要更新为真实App Store ID
|
||||
String get appDownloadUrlApple =>
|
||||
'https://apps.apple.com/us/app/atuchat/id1234567890'; // 需要更新为真实App Store ID
|
||||
|
||||
@override
|
||||
String get anchorAgentUrl => 'https://h5.haiyihy.com/apply/index.html'; // 正式 H5 页面
|
||||
@ -44,7 +46,8 @@ class SCVariant1Config implements AppConfig {
|
||||
String get bdCenterUrl => 'https://h5.haiyihy.com/bd-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get bdLeaderUrl => 'https://h5.haiyihy.com/bd-leader-center/index.html'; // 正式 H5 页面
|
||||
String get bdLeaderUrl =>
|
||||
'https://h5.haiyihy.com/bd-leader-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get coinSellerUrl => 'https://h5.haiyihy.com/coin-seller/index.html'; // 正式 H5 页面
|
||||
@ -53,22 +56,27 @@ class SCVariant1Config implements AppConfig {
|
||||
String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get agencyCenterUrl => 'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面
|
||||
String get agencyCenterUrl =>
|
||||
'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get gamesKingUrl => 'https://h5.haiyihy.com/games-king/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get wealthRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Wealth'; // 正式 H5 页面
|
||||
String get wealthRankUrl =>
|
||||
'https://h5.haiyihy.com/ranking/index.html?first=Wealth'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get charmRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Charm'; // 正式 H5 页面
|
||||
String get charmRankUrl =>
|
||||
'https://h5.haiyihy.com/ranking/index.html?first=Charm'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get roomRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Room'; // 正式 H5 页面
|
||||
String get roomRankUrl =>
|
||||
'https://h5.haiyihy.com/ranking/index.html?first=Room'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get inviteNewUserUrl => 'https://h5.haiyihy.com/invitation/invite-new-user/index.html'; // 正式 H5 页面
|
||||
String get inviteNewUserUrl =>
|
||||
'https://h5.haiyihy.com/invitation/invite-new-user/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
int get primaryColor => 0xffFF5722; // 不同主色(橙色)
|
||||
@ -108,11 +116,11 @@ class SCVariant1Config implements AppConfig {
|
||||
|
||||
@override
|
||||
Map<String, bool> get featureFlags => {
|
||||
'giftSpecialEffects': isGiftSpecialEffects,
|
||||
'entryVehicleAnimation': isEntryVehicleAnimation,
|
||||
'floatingAnimationInGlobal': isFloatingAnimationInGlobal,
|
||||
'luckGiftSpecialEffects': isLuckGiftSpecialEffects,
|
||||
};
|
||||
'giftSpecialEffects': isGiftSpecialEffects,
|
||||
'entryVehicleAnimation': isEntryVehicleAnimation,
|
||||
'floatingAnimationInGlobal': isFloatingAnimationInGlobal,
|
||||
'luckGiftSpecialEffects': isLuckGiftSpecialEffects,
|
||||
};
|
||||
|
||||
@override
|
||||
BusinessLogicStrategy get businessLogicStrategy {
|
||||
@ -158,16 +166,10 @@ class SCVariant1Config implements AppConfig {
|
||||
if (errors.isNotEmpty) {
|
||||
final errorMessage = '应用配置验证失败:\n${errors.join('\n')}';
|
||||
|
||||
if (kDebugMode) {
|
||||
// 调试模式下只输出警告,不阻止应用启动(方便开发)
|
||||
debugPrint('⚠️ 警告: $errorMessage');
|
||||
debugPrint('⚠️ 在调试模式下,应用将继续启动,但某些功能可能无法正常工作');
|
||||
} else {
|
||||
if (!kDebugMode) {
|
||||
// 发布模式下抛出异常,阻止应用启动
|
||||
throw StateError(errorMessage);
|
||||
}
|
||||
} else {
|
||||
debugPrint('应用配置验证通过');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
import '../../../modules/home/popular/event/home_event_page.dart';
|
||||
import '../../../modules/home/popular/mine/sc_home_mine_page.dart';
|
||||
import '../../../modules/home/popular/party/sc_home_party_page.dart';
|
||||
|
||||
@ -50,7 +49,6 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy {
|
||||
// 马甲包:显示更详细的首充对话框
|
||||
SCDialogUtils.showFirstRechargeDialog(context);
|
||||
// 额外逻辑:记录点击事件或触发其他行为
|
||||
debugPrint('马甲包首充提示被点击');
|
||||
}
|
||||
|
||||
/// === 登录页面差异化方法实现(马甲包专属样式)===
|
||||
|
||||
@ -3,6 +3,9 @@ import 'package:yumi/app/config/business_logic_strategy.dart';
|
||||
|
||||
class SCGlobalConfig {
|
||||
static const Set<String> _knownLowPerformanceModels = {"redmi 14c"};
|
||||
static const String floatingBroadcastScopeOff = "off";
|
||||
static const String floatingBroadcastScopeRoom = "room";
|
||||
static const String floatingBroadcastScopeAll = "all";
|
||||
|
||||
static String get apiHost => AppConfig.current.apiHost;
|
||||
static String get imgHost => AppConfig.current.imgHost;
|
||||
@ -130,9 +133,51 @@ class SCGlobalConfig {
|
||||
|
||||
///全局飘屏
|
||||
static bool _isFloatingAnimationInGlobal = true;
|
||||
static String _floatingBroadcastScope = floatingBroadcastScopeAll;
|
||||
static bool get isFloatingAnimationInGlobal => _isFloatingAnimationInGlobal;
|
||||
static set isFloatingAnimationInGlobal(bool value) =>
|
||||
_isFloatingAnimationInGlobal = value;
|
||||
static set isFloatingAnimationInGlobal(bool value) {
|
||||
_isFloatingAnimationInGlobal = value;
|
||||
_floatingBroadcastScope =
|
||||
value ? floatingBroadcastScopeAll : floatingBroadcastScopeOff;
|
||||
}
|
||||
|
||||
static String get floatingBroadcastScope => _floatingBroadcastScope;
|
||||
static set floatingBroadcastScope(String value) {
|
||||
_floatingBroadcastScope = normalizeFloatingBroadcastScope(value);
|
||||
_isFloatingAnimationInGlobal =
|
||||
_floatingBroadcastScope != floatingBroadcastScopeOff;
|
||||
}
|
||||
|
||||
static bool get isFloatingBroadcastClosed =>
|
||||
_floatingBroadcastScope == floatingBroadcastScopeOff;
|
||||
static bool get isFloatingBroadcastRoomOnly =>
|
||||
_floatingBroadcastScope == floatingBroadcastScopeRoom;
|
||||
static bool get isFloatingBroadcastAll =>
|
||||
_floatingBroadcastScope == floatingBroadcastScopeAll;
|
||||
|
||||
static String normalizeFloatingBroadcastScope(String? value) {
|
||||
switch (value) {
|
||||
case floatingBroadcastScopeOff:
|
||||
case floatingBroadcastScopeRoom:
|
||||
case floatingBroadcastScopeAll:
|
||||
return value!;
|
||||
default:
|
||||
return floatingBroadcastScopeAll;
|
||||
}
|
||||
}
|
||||
|
||||
static String clampFloatingBroadcastScope(String value) {
|
||||
final scope = normalizeFloatingBroadcastScope(value);
|
||||
return _isLowPerformanceDevice ? floatingBroadcastScopeOff : scope;
|
||||
}
|
||||
|
||||
static String floatingBroadcastScopeStorageKey(String? account) {
|
||||
return "$account-FloatingBroadcastScope";
|
||||
}
|
||||
|
||||
static String legacyFloatingAnimationInGlobalStorageKey(String? account) {
|
||||
return "$account-FloatingAnimationInGlobal";
|
||||
}
|
||||
|
||||
///幸运礼物特效开关
|
||||
static bool _isLuckGiftSpecialEffects = true;
|
||||
|
||||
@ -1,113 +1,117 @@
|
||||
class SCRoomMsgType {
|
||||
static const String text = "TEXT";
|
||||
static const String image = "IMAGE";
|
||||
static const String joinRoom = "JOIN_ROOM";
|
||||
static const String systemTips = "SYSTEM_TIPS";
|
||||
|
||||
///表情包
|
||||
static const String emoticons = "EMOTICONS";
|
||||
|
||||
|
||||
///新用户
|
||||
static const String newUser = "NEW_USER";
|
||||
|
||||
///上麦
|
||||
static const String shangMai = "SHANG_MAI";
|
||||
|
||||
///下麦
|
||||
static const String xiaMai = "XIA_MAI";
|
||||
|
||||
///踢下麦
|
||||
static const String killXiaMai = "KILL_XIA_MAI";
|
||||
|
||||
///请出房间
|
||||
static const String qcfj = "QCFJ";
|
||||
|
||||
///封麦
|
||||
static const String fengMai = "FENG_MAI";
|
||||
|
||||
///解封
|
||||
static const String jieFeng = "JIE_FENG";
|
||||
|
||||
///禁麦
|
||||
static const String jinMai = "JIN_MAI";
|
||||
|
||||
///解禁
|
||||
static const String jieJin = "JIE_JIN";
|
||||
|
||||
///送礼
|
||||
static const String gift = "GIFT";
|
||||
|
||||
///幸运礼物飘向麦位
|
||||
static const String luckGiftAnimOther = "LUCK_GIFT_ANIM_OTHER";
|
||||
|
||||
///送礼
|
||||
static const String allGift = "ALL_GIFT";
|
||||
|
||||
///收到礼物
|
||||
static const String reciverGift = "RECIVER_GIFT";
|
||||
|
||||
///抱上麦
|
||||
static const String bsm = "BSM";
|
||||
|
||||
///欢迎语
|
||||
static const String welcome = "WELCOME";
|
||||
|
||||
///禁言变动
|
||||
static const String jinyanList = "JINYAN_LIST";
|
||||
|
||||
///麦位变动
|
||||
static const String micChange = "MIC_CHANGE";
|
||||
|
||||
///房间身份变动
|
||||
static const String roomRoleChange = "ROOM_ROLE_CHANGE";
|
||||
|
||||
///火箭进度条更新
|
||||
static const String rocketEnergyUpdate = "ROCKET_ENERGY_UPDSCE";
|
||||
|
||||
///房间红包
|
||||
static const String roomRedPacket = "ROOM_RED_PACKET";
|
||||
|
||||
///邀请进入房间
|
||||
static const String inviteRoom = "INVITE_ROOM";
|
||||
|
||||
///用户火箭中奖
|
||||
static const String rocketRewardUser = "ROCKET_REWARD_USER";
|
||||
|
||||
///管理变动
|
||||
static const String managerList = "MANAGER_LIST";
|
||||
|
||||
///房间设置更新
|
||||
static const String roomSettingUpdate = "ROOM_SETTING_UPDSCE";
|
||||
|
||||
///掷骰子
|
||||
static const String roomDice = "ROOM_DICE";
|
||||
|
||||
///石头剪刀布
|
||||
static const String roomRPS = "ROOM_RPS";
|
||||
|
||||
///幸运数字
|
||||
static const String roomLuckNumber = "ROOM_LUCK_NUMBER";
|
||||
|
||||
///房间背景更新
|
||||
static const String roomBGUpdate = "ROOM_BG_UPDSCE";
|
||||
|
||||
///幸运礼物
|
||||
static const String gameLuckyGift = "GAME_LUCKY_GIFT";
|
||||
|
||||
///幸运礼物中奖5倍以及以上
|
||||
static const String gameLuckyGift_5 = "GAME_LUCKY_GIFT_5";
|
||||
|
||||
///房间多人游戏关闭
|
||||
static const String roomGameClose = "ROOM_GAME_CLOSE";
|
||||
|
||||
///房间游戏创建
|
||||
static const String roomGameCreate = "ROOM_GAME_CRESCE";
|
||||
|
||||
///暂时不用监听这个
|
||||
static const String sendGift = "SEND_GIFT";
|
||||
static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT";
|
||||
static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX";
|
||||
static const String refreshOnlineUser = "REFRESH_ONLINE_USER";
|
||||
|
||||
}
|
||||
class SCRoomMsgType {
|
||||
static const String text = "TEXT";
|
||||
static const String image = "IMAGE";
|
||||
static const String joinRoom = "JOIN_ROOM";
|
||||
static const String systemTips = "SYSTEM_TIPS";
|
||||
|
||||
///表情包
|
||||
static const String emoticons = "EMOTICONS";
|
||||
|
||||
///新用户
|
||||
static const String newUser = "NEW_USER";
|
||||
|
||||
///上麦
|
||||
static const String shangMai = "SHANG_MAI";
|
||||
|
||||
///下麦
|
||||
static const String xiaMai = "XIA_MAI";
|
||||
|
||||
///踢下麦
|
||||
static const String killXiaMai = "KILL_XIA_MAI";
|
||||
|
||||
///请出房间
|
||||
static const String qcfj = "QCFJ";
|
||||
|
||||
///封麦
|
||||
static const String fengMai = "FENG_MAI";
|
||||
|
||||
///解封
|
||||
static const String jieFeng = "JIE_FENG";
|
||||
|
||||
///禁麦
|
||||
static const String jinMai = "JIN_MAI";
|
||||
|
||||
///解禁
|
||||
static const String jieJin = "JIE_JIN";
|
||||
|
||||
///送礼
|
||||
static const String gift = "GIFT";
|
||||
|
||||
///幸运礼物飘向麦位
|
||||
static const String luckGiftAnimOther = "LUCK_GIFT_ANIM_OTHER";
|
||||
|
||||
///送礼
|
||||
static const String allGift = "ALL_GIFT";
|
||||
|
||||
///收到礼物
|
||||
static const String reciverGift = "RECIVER_GIFT";
|
||||
|
||||
///抱上麦
|
||||
static const String bsm = "BSM";
|
||||
|
||||
///欢迎语
|
||||
static const String welcome = "WELCOME";
|
||||
|
||||
///禁言变动
|
||||
static const String jinyanList = "JINYAN_LIST";
|
||||
|
||||
///麦位变动
|
||||
static const String micChange = "MIC_CHANGE";
|
||||
|
||||
///房间背景音乐播放状态
|
||||
static const String roomMusic = "ROOM_MUSIC";
|
||||
|
||||
///房间身份变动
|
||||
static const String roomRoleChange = "ROOM_ROLE_CHANGE";
|
||||
|
||||
///火箭进度条更新
|
||||
static const String rocketEnergyUpdate = "ROCKET_ENERGY_UPDSCE";
|
||||
|
||||
///房间红包
|
||||
static const String roomRedPacket = "ROOM_RED_PACKET";
|
||||
|
||||
///房间红包领取通知
|
||||
static const String roomRedPacketClaim = "ROOM_RED_PACKET_CLAIM";
|
||||
|
||||
///邀请进入房间
|
||||
static const String inviteRoom = "INVITE_ROOM";
|
||||
|
||||
///用户火箭中奖
|
||||
static const String rocketRewardUser = "ROCKET_REWARD_USER";
|
||||
|
||||
///管理变动
|
||||
static const String managerList = "MANAGER_LIST";
|
||||
|
||||
///房间设置更新
|
||||
static const String roomSettingUpdate = "ROOM_SETTING_UPDSCE";
|
||||
|
||||
///掷骰子
|
||||
static const String roomDice = "ROOM_DICE";
|
||||
|
||||
///石头剪刀布
|
||||
static const String roomRPS = "ROOM_RPS";
|
||||
|
||||
///幸运数字
|
||||
static const String roomLuckNumber = "ROOM_LUCK_NUMBER";
|
||||
|
||||
///房间背景更新
|
||||
static const String roomBGUpdate = "ROOM_BG_UPDSCE";
|
||||
|
||||
///幸运礼物
|
||||
static const String gameLuckyGift = "GAME_LUCKY_GIFT";
|
||||
|
||||
///幸运礼物中奖5倍以及以上
|
||||
static const String gameLuckyGift_5 = "GAME_LUCKY_GIFT_5";
|
||||
|
||||
///房间多人游戏关闭
|
||||
static const String roomGameClose = "ROOM_GAME_CLOSE";
|
||||
|
||||
///房间游戏创建
|
||||
static const String roomGameCreate = "ROOM_GAME_CRESCE";
|
||||
|
||||
///暂时不用监听这个
|
||||
static const String sendGift = "SEND_GIFT";
|
||||
static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT";
|
||||
static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX";
|
||||
static const String refreshOnlineUser = "REFRESH_ONLINE_USER";
|
||||
}
|
||||
|
||||
@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:yumi/modules/chat/chat_route.dart';
|
||||
import 'package:yumi/modules/auth/login_route.dart';
|
||||
import 'package:yumi/app/routes/sc_lk_application.dart';
|
||||
|
||||
/// 安卓页面切换时长
|
||||
const Duration kAndroidTransitionDuration = Duration(milliseconds: 375);
|
||||
|
||||
/// fluro的路由跳转工具类
|
||||
|
||||
/// 安卓页面切换时长
|
||||
const Duration kAndroidTransitionDuration = Duration(milliseconds: 375);
|
||||
|
||||
/// fluro的路由跳转工具类
|
||||
class SCNavigatorUtils {
|
||||
static bool inChatPage = false;
|
||||
static bool inLoginPage = false;
|
||||
@ -39,109 +39,117 @@ class SCNavigatorUtils {
|
||||
}
|
||||
|
||||
//不需要页面返回值的跳转
|
||||
static Future push(BuildContext context, String path,
|
||||
{bool replace = false,
|
||||
bool clearStack = false,
|
||||
TransitionType? transition,
|
||||
Duration? transitionDuration,
|
||||
RouteTransitionsBuilder? transitionBuilder}) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
static Future push(
|
||||
BuildContext context,
|
||||
String path, {
|
||||
bool replace = false,
|
||||
bool clearStack = false,
|
||||
TransitionType? transition,
|
||||
Duration? transitionDuration,
|
||||
RouteTransitionsBuilder? transitionBuilder,
|
||||
}) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
inLoginPage = path.startsWith(LoginRouter.login);
|
||||
inChatPage = path.startsWith(SCChatRouter.chat);
|
||||
final result = await SCLkApplication.router.navigateTo(context, path,
|
||||
replace: replace,
|
||||
clearStack: clearStack,
|
||||
///在系统语言为ar语会有问题
|
||||
transition: (Platform.isAndroid
|
||||
? null
|
||||
: TransitionType.cupertino),
|
||||
// transitionDuration: transitionDuration ??
|
||||
// (Platform.isAndroid ? kAndroidTransitionDuration : null),
|
||||
// transitionBuilder: transitionBuilder ??
|
||||
// (Platform.isAndroid ? (context, animation, secondaryAnimation, child) {
|
||||
// return ScaleTransition(
|
||||
// scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)),
|
||||
// child: SlideTransition(
|
||||
// position: Tween<Offset>(
|
||||
// begin: const Offset(1.0, 0.0),
|
||||
// end: const Offset(0.0, 0.0),
|
||||
// ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)),
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// } : null)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//需要页面返回值的跳转
|
||||
static pushResult(
|
||||
BuildContext context, String path, Function(Object) function,
|
||||
{bool replace = false,
|
||||
bool clearStack = false,
|
||||
TransitionType? transition}) {
|
||||
FocusScope.of(context).unfocus();
|
||||
inLoginPage = path == LoginRouter.login;
|
||||
inChatPage = path.startsWith(SCChatRouter.chat);
|
||||
SCLkApplication.router
|
||||
.navigateTo(context, path,
|
||||
replace: replace,
|
||||
clearStack: clearStack,
|
||||
// transition: transition ??
|
||||
// (Platform.isAndroid
|
||||
// ? TransitionType.custom
|
||||
// : TransitionType.cupertino),
|
||||
// transitionDuration:
|
||||
// Platform.isAndroid ? kAndroidTransitionDuration : null,
|
||||
// transitionBuilder:
|
||||
// Platform.isAndroid ? (context, animation, secondaryAnimation, child) {
|
||||
// return ScaleTransition(
|
||||
// scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)),
|
||||
// child: SlideTransition(
|
||||
// position: Tween<Offset>(
|
||||
// begin: const Offset(1.0, 0.0),
|
||||
// end: const Offset(0.0, 0.0),
|
||||
// ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)),
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// } : null
|
||||
)
|
||||
.then((result) {
|
||||
// 页面返回result为null
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
function(result);
|
||||
}).catchError((error) {
|
||||
debugPrint('$error');
|
||||
});
|
||||
final result = await SCLkApplication.router.navigateTo(
|
||||
context,
|
||||
path,
|
||||
replace: replace,
|
||||
clearStack: clearStack,
|
||||
|
||||
///在系统语言为ar语会有问题
|
||||
transition: (Platform.isAndroid ? null : TransitionType.cupertino),
|
||||
// transitionDuration: transitionDuration ??
|
||||
// (Platform.isAndroid ? kAndroidTransitionDuration : null),
|
||||
// transitionBuilder: transitionBuilder ??
|
||||
// (Platform.isAndroid ? (context, animation, secondaryAnimation, child) {
|
||||
// return ScaleTransition(
|
||||
// scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)),
|
||||
// child: SlideTransition(
|
||||
// position: Tween<Offset>(
|
||||
// begin: const Offset(1.0, 0.0),
|
||||
// end: const Offset(0.0, 0.0),
|
||||
// ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)),
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// } : null)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 直接返回
|
||||
static void goBack(BuildContext context) {
|
||||
if(inChatPage){
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
/// 带参数返回
|
||||
static void goBackWithParams(BuildContext context, result) {
|
||||
if(inChatPage){
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
|
||||
static void popUntil(BuildContext context, RoutePredicate predicate) {
|
||||
if(inChatPage){
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.popUntil(context, predicate);
|
||||
}
|
||||
}
|
||||
|
||||
//需要页面返回值的跳转
|
||||
static pushResult(
|
||||
BuildContext context,
|
||||
String path,
|
||||
Function(Object) function, {
|
||||
bool replace = false,
|
||||
bool clearStack = false,
|
||||
TransitionType? transition,
|
||||
}) {
|
||||
FocusScope.of(context).unfocus();
|
||||
inLoginPage = path == LoginRouter.login;
|
||||
inChatPage = path.startsWith(SCChatRouter.chat);
|
||||
SCLkApplication.router
|
||||
.navigateTo(
|
||||
context,
|
||||
path,
|
||||
replace: replace,
|
||||
clearStack: clearStack,
|
||||
// transition: transition ??
|
||||
// (Platform.isAndroid
|
||||
// ? TransitionType.custom
|
||||
// : TransitionType.cupertino),
|
||||
// transitionDuration:
|
||||
// Platform.isAndroid ? kAndroidTransitionDuration : null,
|
||||
// transitionBuilder:
|
||||
// Platform.isAndroid ? (context, animation, secondaryAnimation, child) {
|
||||
// return ScaleTransition(
|
||||
// scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)),
|
||||
// child: SlideTransition(
|
||||
// position: Tween<Offset>(
|
||||
// begin: const Offset(1.0, 0.0),
|
||||
// end: const Offset(0.0, 0.0),
|
||||
// ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)),
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// } : null
|
||||
)
|
||||
.then((result) {
|
||||
// 页面返回result为null
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
function(result);
|
||||
})
|
||||
.catchError((error) {});
|
||||
}
|
||||
|
||||
/// 直接返回
|
||||
static void goBack(BuildContext context) {
|
||||
if (inChatPage) {
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
/// 带参数返回
|
||||
static void goBackWithParams(BuildContext context, result) {
|
||||
if (inChatPage) {
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
|
||||
static void popUntil(BuildContext context, RoutePredicate predicate) {
|
||||
if (inChatPage) {
|
||||
inChatPage = false;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.popUntil(context, predicate);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ class SCRoutes {
|
||||
/// 指定路由跳转错误返回页
|
||||
router.notFoundHandler = fluro.Handler(
|
||||
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
|
||||
debugPrint('未找到目标页');
|
||||
return SCWidgetNotFound();
|
||||
},
|
||||
);
|
||||
|
||||
@ -397,6 +397,16 @@ class SCAppLocalizations {
|
||||
String get floatingAnimationInGlobal =>
|
||||
translate('floatingAnimationInGlobal');
|
||||
|
||||
String get broadcast => translate('broadcast');
|
||||
|
||||
String get broadcastDisplay => translate('broadcastDisplay');
|
||||
|
||||
String get broadcastAllPlaces => translate('broadcastAllPlaces');
|
||||
|
||||
String get broadcastRoomOnly => translate('broadcastRoomOnly');
|
||||
|
||||
String get broadcastOff => translate('broadcastOff');
|
||||
|
||||
String get theRedEnvelopeHasExpired => translate('theRedEnvelopeHasExpired');
|
||||
|
||||
String get wishingYouHappinessEveryDay =>
|
||||
@ -610,6 +620,109 @@ class SCAppLocalizations {
|
||||
|
||||
String get redEnvelope => translate('redEnvelope');
|
||||
|
||||
String get roomRedPacketGoldCoins => translate('roomRedPacketGoldCoins');
|
||||
|
||||
String get roomRedPacketQuantity => translate('roomRedPacketQuantity');
|
||||
|
||||
String get roomRedPacketPickupTime => translate('roomRedPacketPickupTime');
|
||||
|
||||
String get roomRedPacketClaimNow => translate('roomRedPacketClaimNow');
|
||||
|
||||
String get roomRedPacketSendLuckyPack =>
|
||||
translate('roomRedPacketSendLuckyPack');
|
||||
|
||||
String get roomRedPacketSending => translate('roomRedPacketSending');
|
||||
|
||||
String get roomRedPacketSendSuccess => translate('roomRedPacketSendSuccess');
|
||||
|
||||
String get roomRedPacketComingSoon => translate('roomRedPacketComingSoon');
|
||||
|
||||
String roomRedPacketInMinutes(String minutes) =>
|
||||
translate('roomRedPacketInMinutes').replaceAll('{1}', minutes);
|
||||
|
||||
String get roomRedPacketRulesTitle => translate('roomRedPacketRulesTitle');
|
||||
|
||||
String roomRedPacketRulesContent(String dailyLimit) => translate(
|
||||
'roomRedPacketRulesContent',
|
||||
).replaceAll('{1}', dailyLimit).replaceAll('%1\$s', dailyLimit);
|
||||
|
||||
String get roomRedPacketHistoryTitle =>
|
||||
translate('roomRedPacketHistoryTitle');
|
||||
|
||||
String roomRedPacketHistoryItemTitle(String count) =>
|
||||
translate('roomRedPacketHistoryItemTitle').replaceAll('{1}', count);
|
||||
|
||||
String get roomRedPacketHistoryStatusSent =>
|
||||
translate('roomRedPacketHistoryStatusSent');
|
||||
|
||||
String get roomRedPacketHistoryStatusFinished =>
|
||||
translate('roomRedPacketHistoryStatusFinished');
|
||||
|
||||
String get roomRedPacketHistoryStatusPending =>
|
||||
translate('roomRedPacketHistoryStatusPending');
|
||||
|
||||
String get roomRedPacketHistoryStatusReturned =>
|
||||
translate('roomRedPacketHistoryStatusReturned');
|
||||
|
||||
String roomRedPacketHistoryRefund(String amount) =>
|
||||
translate('roomRedPacketHistoryRefund').replaceAll('{1}', amount);
|
||||
|
||||
String get roomRedPacketClaimRecordsTitle =>
|
||||
translate('roomRedPacketClaimRecordsTitle');
|
||||
|
||||
String get roomRedPacketChatMessage => translate('roomRedPacketChatMessage');
|
||||
|
||||
String roomRedPacketReceivedMessage(String amount, String name) => translate(
|
||||
'roomRedPacketReceivedMessage',
|
||||
).replaceAll('{1}', amount).replaceAll('{2}', name);
|
||||
|
||||
String get roomRedPacketOpenSubtitle =>
|
||||
translate('roomRedPacketOpenSubtitle');
|
||||
|
||||
String get roomRedPacketOpenYouGot => translate('roomRedPacketOpenYouGot');
|
||||
|
||||
String get roomRedPacketClaimSuccessTips =>
|
||||
translate('roomRedPacketClaimSuccessTips');
|
||||
|
||||
String get roomRedPacketOpenSoldOut => translate('roomRedPacketOpenSoldOut');
|
||||
|
||||
String get roomRedPacketOpenExpired => translate('roomRedPacketOpenExpired');
|
||||
|
||||
String get roomRedPacketRegionNotMatch =>
|
||||
translate('roomRedPacketRegionNotMatch');
|
||||
|
||||
String get roomRedPacketNotStarted => translate('roomRedPacketNotStarted');
|
||||
|
||||
String get roomRedPacketDailyLimitExceeded =>
|
||||
translate('roomRedPacketDailyLimitExceeded');
|
||||
|
||||
String get roomRedPacketInsufficientBalance =>
|
||||
translate('roomRedPacketInsufficientBalance');
|
||||
|
||||
String get roomRedPacketPresenceRequired =>
|
||||
translate('roomRedPacketPresenceRequired');
|
||||
|
||||
String get roomRedPacketNetworkError =>
|
||||
translate('roomRedPacketNetworkError');
|
||||
|
||||
String get roomRedPacketAlreadyClaimed =>
|
||||
translate('roomRedPacketAlreadyClaimed');
|
||||
|
||||
String roomRedPacketOpenWaiting(String minutes) =>
|
||||
translate('roomRedPacketOpenWaiting').replaceAll('{1}', minutes);
|
||||
|
||||
String roomRedPacketOpenWaitingCountdown(String countdown) => translate(
|
||||
'roomRedPacketOpenWaitingCountdown',
|
||||
).replaceAll('{1}', countdown);
|
||||
|
||||
String get roomRedPacketOkay => translate('roomRedPacketOkay');
|
||||
|
||||
String roomRedPacketFloatingTitle(String name) =>
|
||||
translate('roomRedPacketFloatingTitle').replaceAll('{1}', name);
|
||||
|
||||
String get roomRedPacketFloatingSubtitle =>
|
||||
translate('roomRedPacketFloatingSubtitle');
|
||||
|
||||
String get dice => translate('dice');
|
||||
|
||||
String get rps => translate('rps');
|
||||
@ -796,6 +909,10 @@ class SCAppLocalizations {
|
||||
|
||||
String get useMicrophoneForOneMin => translate('useMicrophoneForOneMin');
|
||||
|
||||
String get taskRequirement => translate('taskRequirement');
|
||||
|
||||
String get taskProgress => translate('taskProgress');
|
||||
|
||||
String get rewardClaimedSuccessfully =>
|
||||
translate('rewardClaimedSuccessfully');
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ class ImagePick {
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
if (!SCPathUtils.fileTypeIsPicAndMP4(pickedFile.path)) {
|
||||
SCTts.show( "Please select sc_images in .jpg, .jpeg, .png format.");
|
||||
SCTts.show("Please select sc_images in .jpg, .jpeg, .png format.");
|
||||
return null;
|
||||
}
|
||||
return [File(pickedFile.path)];
|
||||
@ -31,7 +31,6 @@ class ImagePick {
|
||||
return null;
|
||||
} catch (e) {
|
||||
// 处理异常,例如权限被拒绝或选择器被取消
|
||||
print("图片选择出错: $e");
|
||||
// 可以在这里添加用户友好的提示信息
|
||||
return null;
|
||||
}
|
||||
@ -58,7 +57,6 @@ class ImagePick {
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print("多图选择出错: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -74,14 +72,13 @@ class ImagePick {
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) {
|
||||
SCTts.show( "Please select sc_images in .jpg, .jpeg, .png format.");
|
||||
SCTts.show("Please select sc_images in .jpg, .jpeg, .png format.");
|
||||
return null;
|
||||
}
|
||||
return File(pickedFile.path);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print("图片选择出错: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -100,7 +97,6 @@ class ImagePick {
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print("相机拍照出错: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import 'package:fluro/fluro.dart' as fluro;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@ -126,33 +125,25 @@ Future<void> _warmUpDeferredServices() async {
|
||||
}
|
||||
_isCrashlyticsReady = true;
|
||||
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
|
||||
if (kDebugMode) {
|
||||
debugPrint('Firebase/Crashlytics 后台初始化完成');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Firebase 后台初始化异常: $e\n$stackTrace');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _recordFlutterError(FlutterErrorDetails details) async {
|
||||
if (!_isCrashlyticsReady) {
|
||||
debugPrint(
|
||||
'Flutter Error before Crashlytics ready: ${details.exceptionAsString()}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await FirebaseCrashlytics.instance.recordFlutterFatalError(details);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Crashlytics 记录 Flutter 错误失败: $e\n$stackTrace');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _recordFatalError(Object error, StackTrace stackTrace) async {
|
||||
if (!_isCrashlyticsReady) {
|
||||
debugPrint('Unhandled error before Crashlytics ready: $error');
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -162,8 +153,8 @@ Future<void> _recordFatalError(Object error, StackTrace stackTrace) async {
|
||||
stackTrace,
|
||||
fatal: true,
|
||||
);
|
||||
} catch (e, recordStackTrace) {
|
||||
debugPrint('Crashlytics 记录异常失败: $e\n$recordStackTrace');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,14 +169,8 @@ Future<void> _initStore() async {
|
||||
try {
|
||||
await DataPersistence.initialize();
|
||||
success = true;
|
||||
if (kDebugMode) {
|
||||
debugPrint('数据存储初始化成功(尝试次数: $attemptCount)');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('数据存储初始化尝试 $attemptCount 失败: $e');
|
||||
|
||||
} catch (_) {
|
||||
if (attemptCount >= maxAttempts) {
|
||||
debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts');
|
||||
// 不抛出异常,允许应用继续运行
|
||||
return;
|
||||
}
|
||||
@ -337,7 +322,6 @@ class _YumiApplicationState extends State<YumiApplication>
|
||||
|
||||
void _handleLink(Uri uri) {
|
||||
// 这是处理链接的核心路由逻辑
|
||||
debugPrint('App 根层收到链接: $uri');
|
||||
}
|
||||
|
||||
void _initRouter() {
|
||||
|
||||
@ -640,9 +640,7 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
|
||||
authType = SCAuthType.GOOGLE.name;
|
||||
idToken = googleUid;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('resolve register auth failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (authType.isEmpty || idToken.isEmpty) {
|
||||
SCLoadingManager.hide();
|
||||
@ -715,6 +713,7 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
|
||||
userProvider?.syncCurrentUserProfile(user.userProfile);
|
||||
await DataPersistence.clearPendingRegisterRewardDialog();
|
||||
await DataPersistence.setAwaitRegisterRewardSocket(true);
|
||||
await DataPersistence.setPendingFirstRegisterRoomGameEvent(true);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -162,7 +162,6 @@ class _MessageItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
this.context = context;
|
||||
print('status:${message.status}');
|
||||
String time = SCMDateUtils.formatMessageTime(
|
||||
context,
|
||||
DateTime.fromMillisecondsSinceEpoch(int.parse(message.createdAt ?? "0")),
|
||||
|
||||
@ -21,6 +21,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -53,7 +54,9 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
@ -61,6 +64,37 @@ import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
|
||||
|
||||
const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
);
|
||||
const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2);
|
||||
|
||||
String _resolveVipChatBubbleImageUrl(SCVipResourceRes? resource) {
|
||||
final candidates = <String?>[
|
||||
resource?.sourceResourceUrl,
|
||||
resource?.previewUrl,
|
||||
resource?.coverUrl,
|
||||
resource?.cover,
|
||||
];
|
||||
return _firstNonBlankChatBubbleUrl([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstNonBlankChatBubbleUrl(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
class SCMessageChatPage extends StatefulWidget {
|
||||
final V2TimConversation? conversation;
|
||||
final bool shrinkWrap;
|
||||
@ -101,6 +135,8 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
///互相关注才能互相发送消息
|
||||
bool canSendMsg = false;
|
||||
SocialChatUserProfile? friend;
|
||||
SCVipResourceRes? _selfVipChatBubble;
|
||||
bool _isSelfVipChatBubbleLoading = false;
|
||||
|
||||
///是否显示发送按钮
|
||||
bool showSend = false;
|
||||
@ -150,6 +186,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
rtmProvider?.onRevokeMessageListener = _onRevokeMessage;
|
||||
rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage;
|
||||
loadMsg();
|
||||
_loadSelfVipChatBubble();
|
||||
|
||||
if (_isC2CConversation && friend == null) {
|
||||
loadFriend();
|
||||
@ -337,22 +374,183 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSelfVipChatBubble() async {
|
||||
if (_isSelfVipChatBubbleLoading) return;
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if (currentUserId == null || currentUserId.isEmpty) return;
|
||||
|
||||
_isSelfVipChatBubbleLoading = true;
|
||||
final repository = SCVipRepositoryImp();
|
||||
SCVipHomeRes? vipHome;
|
||||
SCVipResourceRes? chatBubble;
|
||||
|
||||
try {
|
||||
final home = await repository.vipHome();
|
||||
vipHome = home;
|
||||
chatBubble = _resolveSelfVipChatBubbleFromHome(home);
|
||||
} catch (error) {}
|
||||
|
||||
if (chatBubble == null) {
|
||||
try {
|
||||
final status = await repository.vipStatus();
|
||||
chatBubble = _resolveSelfVipChatBubbleFromStatus(status);
|
||||
if (chatBubble == null &&
|
||||
vipHome != null &&
|
||||
_isUsableVipStatus(status)) {
|
||||
chatBubble = _firstUsableVipChatBubble([
|
||||
_currentVipLevelConfig(
|
||||
vipHome,
|
||||
targetLevel: status.levelInt,
|
||||
targetLevelCode: status.levelCode,
|
||||
)?.chatBubble,
|
||||
]);
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_selfVipChatBubble = chatBubble;
|
||||
_isSelfVipChatBubbleLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromHome(SCVipHomeRes home) {
|
||||
final status = home.state;
|
||||
if (status?.active == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final config = _currentVipLevelConfig(home);
|
||||
if (_isUsableVipStatus(status)) {
|
||||
return _firstUsableVipChatBubble([
|
||||
status?.chatBubble,
|
||||
config?.chatBubble,
|
||||
]);
|
||||
}
|
||||
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
if (profileLevel > 0) {
|
||||
return _firstUsableVipChatBubble([config?.chatBubble]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _resolveSelfVipChatBubbleFromStatus(SCVipStatusRes status) {
|
||||
if (!_isUsableVipStatus(status)) {
|
||||
return null;
|
||||
}
|
||||
return _firstUsableVipChatBubble([status.chatBubble]);
|
||||
}
|
||||
|
||||
SCVipLevelConfigRes? _currentVipLevelConfig(
|
||||
SCVipHomeRes home, {
|
||||
int? targetLevel,
|
||||
String? targetLevelCode,
|
||||
}) {
|
||||
final status = home.state;
|
||||
final statusLevel = _vipLevelIntFromStatus(status);
|
||||
final profileLevel =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ??
|
||||
0;
|
||||
final resolvedTargetLevel =
|
||||
(targetLevel ?? 0) > 0
|
||||
? targetLevel!
|
||||
: (statusLevel > 0 ? statusLevel : profileLevel);
|
||||
final statusLevelCode =
|
||||
targetLevelCode?.trim() ?? status?.levelCode?.trim();
|
||||
|
||||
for (final level in home.levels) {
|
||||
if (resolvedTargetLevel > 0 && level.levelInt == resolvedTargetLevel) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
if (statusLevelCode != null && statusLevelCode.isNotEmpty) {
|
||||
for (final level in home.levels) {
|
||||
if (_sameVipLevelCode(level.levelCode, statusLevelCode)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isUsableVipStatus(SCVipStatusRes? status) {
|
||||
return status != null &&
|
||||
status.active != false &&
|
||||
(_vipLevelIntFromStatus(status) > 0 ||
|
||||
_hasNonBlankText(status.levelCode) ||
|
||||
_hasNonBlankText(status.displayName) ||
|
||||
_resolveVipChatBubbleImageUrl(status.chatBubble).isNotEmpty);
|
||||
}
|
||||
|
||||
int _vipLevelIntFromStatus(SCVipStatusRes? status) {
|
||||
final explicitLevel = status?.levelInt ?? 0;
|
||||
if (explicitLevel > 0) {
|
||||
return explicitLevel;
|
||||
}
|
||||
return _parseVipLevelText(status?.levelCode) ??
|
||||
_parseVipLevelText(status?.displayName) ??
|
||||
0;
|
||||
}
|
||||
|
||||
int? _parseVipLevelText(String? value) {
|
||||
final text = value?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final match = RegExp(
|
||||
r'(?:S?VIP)?\s*([1-9]\d*)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
return int.tryParse(match?.group(1) ?? '');
|
||||
}
|
||||
|
||||
bool _sameVipLevelCode(String? left, String? right) {
|
||||
final normalizedLeft = left?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
final normalizedRight = right?.replaceAll(RegExp(r'\s+'), '').toLowerCase();
|
||||
return normalizedLeft != null &&
|
||||
normalizedLeft.isNotEmpty &&
|
||||
normalizedLeft == normalizedRight;
|
||||
}
|
||||
|
||||
bool _hasNonBlankText(String? value) {
|
||||
return value?.trim().isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
SCVipResourceRes? _firstUsableVipChatBubble(
|
||||
Iterable<SCVipResourceRes?> resources,
|
||||
) {
|
||||
for (final resource in resources) {
|
||||
if (_resolveVipChatBubbleImageUrl(resource).isNotEmpty) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _vipChatBubbleLogValue(SCVipResourceRes? resource) {
|
||||
if (resource == null) return 'null';
|
||||
return '{id:${resource.resourceId},name:${resource.name},'
|
||||
'previewUrl:${resource.previewUrl},'
|
||||
'sourceUrl:${resource.sourceResourceUrl},'
|
||||
'selectedUrl:${_resolveVipChatBubbleImageUrl(resource)}}';
|
||||
}
|
||||
|
||||
///消息列表
|
||||
Widget _msgList() {
|
||||
print('xiaoxiliebiao:${currentConversationMessageList.length}');
|
||||
return SmartRefresher(
|
||||
enablePullDown: false,
|
||||
enablePullUp: true,
|
||||
onLoading: () async {
|
||||
print('onLoading');
|
||||
if (currentConversationMessageList.isNotEmpty) {
|
||||
final messages = await _loadHistoryMessages(
|
||||
count: 30,
|
||||
lastMsgID: currentConversationMessageList.last.msgID,
|
||||
);
|
||||
print('加载前:${currentConversationMessageList.length}');
|
||||
currentConversationMessageList.addAll(messages);
|
||||
print('加载后:${currentConversationMessageList.length}');
|
||||
if (messages.length == 30) {
|
||||
_refreshController.loadComplete();
|
||||
} else {
|
||||
@ -385,6 +583,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(c, i) => _MessageItem(
|
||||
message: currentConversationMessageList[i],
|
||||
selfVipChatBubble: _selfVipChatBubble,
|
||||
preMessage:
|
||||
i < currentConversationMessageList.length - 1
|
||||
? currentConversationMessageList[i + 1]
|
||||
@ -472,7 +671,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
),
|
||||
);
|
||||
final messages = await _loadHistoryMessages(count: 100);
|
||||
print("messages : ${messages.length}");
|
||||
currentConversationMessageList.clear();
|
||||
|
||||
for (var msg in messages) {
|
||||
@ -868,6 +1066,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
class _MessageItem extends StatelessWidget {
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
final SCVipResourceRes? selfVipChatBubble;
|
||||
|
||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||
|
||||
@ -882,6 +1081,7 @@ class _MessageItem extends StatelessWidget {
|
||||
|
||||
_MessageItem({
|
||||
required this.message,
|
||||
this.selfVipChatBubble,
|
||||
this.preMessage,
|
||||
// this.userProfile,
|
||||
this.isSystem = false,
|
||||
@ -899,7 +1099,6 @@ class _MessageItem extends StatelessWidget {
|
||||
int preTimestamp = (preMessage?.timestamp ?? 0) * 1000;
|
||||
if (preMessage != null) {
|
||||
///5分钟以内 不显示
|
||||
// print('timestamp:$timestamp===preTimestamp:${preTimestamp}');
|
||||
if (DateTime.fromMillisecondsSinceEpoch(timestamp)
|
||||
.difference(DateTime.fromMillisecondsSinceEpoch(preTimestamp))
|
||||
.inMilliseconds <
|
||||
@ -1259,14 +1458,46 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTextBubble(String content) {
|
||||
final chatBubble = _senderProfile()?.getChatBox();
|
||||
final chatBubble =
|
||||
(message.isSelf ?? false)
|
||||
? _currentUserChatBubble()
|
||||
: _senderProfile()?.getChatBox();
|
||||
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
|
||||
if (chatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
|
||||
}
|
||||
if (message.isSelf ?? false) {
|
||||
final vipChatBubbleUrl = _resolveVipChatBubbleImageUrl(selfVipChatBubble);
|
||||
if (vipChatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(
|
||||
content: content,
|
||||
imageUrl: vipChatBubbleUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
return _buildDefaultTextBubble(content);
|
||||
}
|
||||
|
||||
PropsResources? _currentUserChatBubble() {
|
||||
final memoryBubble =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getChatBox();
|
||||
if (_resolveChatBubbleImageUrl(memoryBubble).isNotEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
|
||||
final userJson = DataPersistence.getCurrentUser();
|
||||
if (userJson.isEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
try {
|
||||
return SocialChatLoginRes.fromJson(
|
||||
jsonDecode(userJson),
|
||||
).userProfile?.getChatBox();
|
||||
} catch (_) {
|
||||
return memoryBubble;
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _senderProfile() {
|
||||
if (message.isSelf ?? false) {
|
||||
return AccountStorage().getCurrentUser()?.userProfile;
|
||||
@ -1307,8 +1538,8 @@ class _MessageItem extends StatelessWidget {
|
||||
260.w,
|
||||
);
|
||||
final multilineMaxTextWidth = math.max(
|
||||
90.w,
|
||||
multilineMaxBubbleWidth - 58.w,
|
||||
82.w,
|
||||
multilineMaxBubbleWidth - 76.w,
|
||||
);
|
||||
final multilineTextPainter = TextPainter(
|
||||
text: TextSpan(text: content, style: textStyle),
|
||||
@ -1338,11 +1569,36 @@ class _MessageItem extends StatelessWidget {
|
||||
required Size textSize,
|
||||
required double maxBubbleWidth,
|
||||
}) {
|
||||
final leftPadding = 34.w;
|
||||
final rightPadding = 42.w;
|
||||
final topPadding = 16.w;
|
||||
final bottomPadding = 18.w;
|
||||
final bubbleWidth = math
|
||||
.max(126.w, textSize.width + 58.w)
|
||||
.max(126.w, textSize.width + leftPadding + rightPadding)
|
||||
.clamp(126.w, maxBubbleWidth);
|
||||
final textMaxWidth = math.max(90.w, bubbleWidth - 58.w);
|
||||
final bubbleHeight = math.max(58.w, textSize.height + 26.w);
|
||||
final textMaxWidth = math.max(
|
||||
64.w,
|
||||
bubbleWidth - leftPadding - rightPadding,
|
||||
);
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: content, style: textStyle),
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: textMaxWidth);
|
||||
final lineMetrics = textPainter.computeLineMetrics();
|
||||
final lineCount = math.max(1, lineMetrics.length);
|
||||
final lineHeight =
|
||||
lineMetrics.isEmpty
|
||||
? textPainter.preferredLineHeight
|
||||
: lineMetrics.fold<double>(
|
||||
0,
|
||||
(height, line) => math.max(height, line.height),
|
||||
);
|
||||
final lineSafetyPadding = lineHeight * math.max(0, lineCount - 1) * 0.55;
|
||||
final textBoxHeight = textPainter.size.height + lineSafetyPadding;
|
||||
final bubbleHeight = math.max(
|
||||
76.w,
|
||||
textBoxHeight + topPadding + bottomPadding,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: bubbleWidth,
|
||||
@ -1354,14 +1610,20 @@ class _MessageItem extends StatelessWidget {
|
||||
url: imageUrl,
|
||||
width: bubbleWidth,
|
||||
height: bubbleHeight,
|
||||
fallback: _buildVipBubbleFallback(),
|
||||
fallback: SizedBox(width: bubbleWidth, height: bubbleHeight),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
leftPadding,
|
||||
topPadding,
|
||||
rightPadding,
|
||||
bottomPadding,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SizedBox(
|
||||
width: textMaxWidth,
|
||||
height: textBoxHeight,
|
||||
child: ExtendedText(content, style: textStyle),
|
||||
),
|
||||
),
|
||||
@ -1376,12 +1638,15 @@ class _MessageItem extends StatelessWidget {
|
||||
required String imageUrl,
|
||||
required TextStyle textStyle,
|
||||
}) {
|
||||
final horizontalPadding = 18.w;
|
||||
final verticalPadding = 8.w;
|
||||
final minSkinWidth = 132.w;
|
||||
final minSkinHeight = 44.w;
|
||||
final leftPadding = 22.w;
|
||||
final rightPadding = 24.w;
|
||||
final topPadding = 12.w;
|
||||
final bottomPadding = 16.w;
|
||||
final minSkinWidth = 50.w;
|
||||
final minSkinHeight = 58.w;
|
||||
final textWidthSafety = 10.w;
|
||||
final maxTextWidth = width(220);
|
||||
final maxBubbleWidth = maxTextWidth + horizontalPadding * 2;
|
||||
final maxBubbleWidth = maxTextWidth + leftPadding + rightPadding;
|
||||
final textSize = _measureTextSize(
|
||||
text: content,
|
||||
style: textStyle,
|
||||
@ -1389,12 +1654,18 @@ class _MessageItem extends StatelessWidget {
|
||||
);
|
||||
final bubbleWidth = math.min(
|
||||
maxBubbleWidth,
|
||||
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
|
||||
math.max(
|
||||
minSkinWidth,
|
||||
textSize.width + textWidthSafety + leftPadding + rightPadding,
|
||||
),
|
||||
);
|
||||
final textMaxWidth = math.max(
|
||||
1.0,
|
||||
bubbleWidth - leftPadding - rightPadding,
|
||||
);
|
||||
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
|
||||
final bubbleHeight = math.max(
|
||||
verticalPadding * 2,
|
||||
textSize.height + verticalPadding * 2,
|
||||
minSkinHeight,
|
||||
textSize.height + topPadding + bottomPadding,
|
||||
);
|
||||
final skinAlignment =
|
||||
message.isSelf!
|
||||
@ -1408,7 +1679,6 @@ class _MessageItem extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(child: _buildVipBubbleFallback()),
|
||||
Positioned.fill(
|
||||
child: _buildVipBubbleSkin(
|
||||
url: imageUrl,
|
||||
@ -1418,15 +1688,22 @@ class _MessageItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
leftPadding,
|
||||
topPadding,
|
||||
rightPadding,
|
||||
bottomPadding,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SizedBox(
|
||||
width: textMaxWidth,
|
||||
child: ExtendedText(content, style: textStyle),
|
||||
child: ExtendedText(
|
||||
content,
|
||||
style: textStyle,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1454,18 +1731,15 @@ class _MessageItem extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
loop: true,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
)
|
||||
: SCNinePatchImage(
|
||||
: _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
0.34,
|
||||
),
|
||||
fallback: SizedBox(width: width, height: height),
|
||||
),
|
||||
);
|
||||
@ -1483,18 +1757,41 @@ class _MessageItem extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
loop: true,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
return SCNinePatchImage(
|
||||
return _buildStaticVipBubbleImage(
|
||||
url: url,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStaticVipBubbleImage({
|
||||
required String url,
|
||||
required double width,
|
||||
required double height,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final imageUrl = url.trim();
|
||||
if (imageUrl.isEmpty) {
|
||||
return fallback ?? SizedBox(width: width, height: height);
|
||||
}
|
||||
|
||||
return SCNinePatchImage(
|
||||
url: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
centerSliceRatio: _vipBubbleCenterSliceRatio,
|
||||
sourceEdgeInset: _vipBubbleSourceEdgeInset,
|
||||
fallback: fallback ?? SizedBox(width: width, height: height),
|
||||
);
|
||||
}
|
||||
|
||||
Size _measureTextSize({
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
@ -1507,24 +1804,22 @@ class _MessageItem extends StatelessWidget {
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
Widget _buildVipBubbleFallback() {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
||||
return _firstNonBlank([
|
||||
final candidates = <String?>[
|
||||
resource?.imSendCoverUrl,
|
||||
resource?.imOpenedUrl,
|
||||
resource?.imOpenedUrlTwo,
|
||||
resource?.imNotOpenedUrl,
|
||||
resource?.roomSendCoverUrl,
|
||||
resource?.roomOpenedUrl,
|
||||
resource?.roomNotOpenedUrl,
|
||||
resource?.cover,
|
||||
resource?.expand,
|
||||
resource?.sourceUrl,
|
||||
];
|
||||
return _firstNonBlank([
|
||||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||||
...candidates,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +165,6 @@ class _MessageItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
this.context = context;
|
||||
print('status:${message.status}');
|
||||
String time = SCMDateUtils.formatMessageTime(
|
||||
context,
|
||||
DateTime.fromMillisecondsSinceEpoch(int.parse(message.createdAt ?? "0")),
|
||||
|
||||
@ -94,7 +94,6 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
||||
);
|
||||
List<V2TimMessage> messages = v2timValueCallback.data!;
|
||||
// List<V2TimMessage> messages = await FTIM.getMessageManager().getMessages(conversation: currentConversation);
|
||||
print("messages : ${messages?.length ?? 0}");
|
||||
currentConversationMessageList ??= [];
|
||||
currentConversationMessageList?.clear();
|
||||
|
||||
@ -175,12 +174,10 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
||||
}
|
||||
|
||||
Widget _msgList() {
|
||||
print('xiaoxiliebiao:${currentConversationMessageList.length}');
|
||||
return SmartRefresher(
|
||||
enablePullDown: false,
|
||||
enablePullUp: true,
|
||||
onLoading: () async {
|
||||
print('onLoading');
|
||||
if (currentConversationMessageList.isNotEmpty) {
|
||||
// 拉取单聊历史消息
|
||||
// 首次拉取,lastMsgID 设置为 null
|
||||
@ -195,9 +192,7 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
||||
);
|
||||
List<V2TimMessage> messages =
|
||||
v2timValueCallback.data as List<V2TimMessage>;
|
||||
print('加载前:${currentConversationMessageList.length}');
|
||||
currentConversationMessageList.addAll(messages);
|
||||
print('加载后:${currentConversationMessageList.length}');
|
||||
if (messages.length == 30) {
|
||||
_refreshController.loadComplete();
|
||||
} else {
|
||||
@ -275,7 +270,6 @@ class _MessageItem extends StatelessWidget {
|
||||
int preTimestamp = (preMessage?.timestamp ?? 0) * 1000;
|
||||
if (preMessage != null) {
|
||||
///5分钟以内 不显示
|
||||
// print('timestamp:$timestamp===preTimestamp:${preTimestamp}');
|
||||
if (DateTime.fromMillisecondsSinceEpoch(timestamp)
|
||||
.difference(DateTime.fromMillisecondsSinceEpoch(preTimestamp))
|
||||
.inMilliseconds <
|
||||
@ -295,11 +289,12 @@ class _MessageItem extends StatelessWidget {
|
||||
final data = jsonDecode(content);
|
||||
noticeType = data["noticeType"];
|
||||
if (noticeType == SCSysytemMessageType.CP_LOVE_LETTER.name) {
|
||||
var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"]));
|
||||
var bean = SCSystemInvitMessageRes.fromJson(
|
||||
jsonDecode(data["content"]),
|
||||
);
|
||||
tagHead = bean.userAvatar ?? "";
|
||||
}
|
||||
}
|
||||
print('status:${message.status}');
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
|
||||
child: Column(
|
||||
@ -359,7 +354,8 @@ class _MessageItem extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
)
|
||||
: ExtendedImage.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getMessagePageSystemMessageIcon(),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getMessagePageSystemMessageIcon(),
|
||||
width: 45.w,
|
||||
shape: BoxShape.circle,
|
||||
fit: BoxFit.cover,
|
||||
@ -417,7 +413,7 @@ class _MessageItem extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(0),
|
||||
topRight: Radius.circular(8.w),
|
||||
@ -464,8 +460,7 @@ class _MessageItem extends StatelessWidget {
|
||||
if ("ACTIVITY" == propType ||
|
||||
"ADMINISTRSCOR" == propType ||
|
||||
"ACHIEVEMENT" == propType) {
|
||||
|
||||
}else {
|
||||
} else {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
StoreRoute.bags,
|
||||
@ -473,7 +468,11 @@ class _MessageItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
SCNavigatorUtils.push(context, StoreRoute.bags, replace: false);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
StoreRoute.bags,
|
||||
replace: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
@ -538,7 +537,8 @@ class _MessageItem extends StatelessWidget {
|
||||
},
|
||||
context,
|
||||
);
|
||||
} else if (type == SCSysytemMessageType.ADMIN_INVITE_RECHARGE_AGENT.name) {
|
||||
} else if (type ==
|
||||
SCSysytemMessageType.ADMIN_INVITE_RECHARGE_AGENT.name) {
|
||||
return SCSystemMessageUtils.buildAdminInviteRechargeAgentMessage(
|
||||
data,
|
||||
(ct, content) {
|
||||
@ -567,7 +567,10 @@ class _MessageItem extends StatelessWidget {
|
||||
context,
|
||||
);
|
||||
} else if (type == SCSysytemMessageType.CP_LOVE_LETTER.name) {
|
||||
return SCSystemMessageUtils.buildCPLoveLetterMessage(data, (ct, content) {
|
||||
return SCSystemMessageUtils.buildCPLoveLetterMessage(data, (
|
||||
ct,
|
||||
content,
|
||||
) {
|
||||
_showMsgItemMenu(ct, content);
|
||||
}, context);
|
||||
} else if (type == SCSysytemMessageType.USER_COINS_RECEIVED.name) {
|
||||
@ -697,7 +700,11 @@ class _MessageItem extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
SCGlobalConfig.businessLogicStrategy.getIdBackgroundImage(bean.account != bean.actualAccount),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getIdBackgroundImage(
|
||||
bean.account !=
|
||||
bean.actualAccount,
|
||||
),
|
||||
),
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
@ -714,7 +721,8 @@ class _MessageItem extends StatelessWidget {
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getCopyIdIcon(),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getCopyIdIcon(),
|
||||
width: 12.w,
|
||||
height: 12.w,
|
||||
),
|
||||
@ -759,8 +767,12 @@ class _MessageItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
SCAccountRepository().followNew(expand).then((result) {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.followSucc);
|
||||
SCAccountRepository().followNew(expand).then((
|
||||
result,
|
||||
) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(context)!.followSucc,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -925,7 +937,8 @@ class _MessageItem extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getSCMessageChatPageMessageMenuDeleteIcon(),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSCMessageChatPageMessageMenuDeleteIcon(),
|
||||
width: 20.w,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
|
||||
@ -106,9 +106,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
Timer? _comboSendBatchTimer;
|
||||
bool _isComboSendBatchInFlight = false;
|
||||
|
||||
void _giftFxLog(String message) {
|
||||
debugPrint('[GiftFX][Send] $message');
|
||||
}
|
||||
void _giftFxLog(String message) {}
|
||||
|
||||
String _describeGiftSendError(Object error) {
|
||||
if (error is DioException) {
|
||||
@ -134,6 +132,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
required int quantity,
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) {
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
@ -175,6 +174,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
user: currentUser,
|
||||
toUser: targetUser,
|
||||
targetUserIds: targetUserIds,
|
||||
giftBatchId: giftBatchId,
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
),
|
||||
@ -1347,23 +1347,28 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||
);
|
||||
if (request.isLuckyGiftRequest) {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
_showLocalLuckyGiftFeedback(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
await sendLuckGiftAnimOtherMsg(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
} else {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
sendGiftMsg(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
}
|
||||
@ -1390,6 +1395,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
required int quantity,
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) {
|
||||
///发送一条IM消息
|
||||
@ -1426,6 +1432,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||||
toUser: u.user,
|
||||
targetUserIds: targetUserIds,
|
||||
giftBatchId: giftBatchId,
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
type: SCRoomMsgType.gift,
|
||||
@ -1452,22 +1459,36 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
if (coins > 9999) {
|
||||
var fMsg = SCFloatingMessage(
|
||||
type: 1,
|
||||
userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||||
toUserId: u.user?.id ?? "",
|
||||
userAvatarUrl:
|
||||
AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "",
|
||||
userName:
|
||||
AccountStorage().getCurrentUser()?.userProfile?.userNickname ??
|
||||
"",
|
||||
toUserName: u.user?.userNickname ?? "",
|
||||
toUserAvatarUrl: u.user?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
coins: coins,
|
||||
number: quantity,
|
||||
);
|
||||
OverlayManager().addMessage(fMsg);
|
||||
_enqueueGiftFloatingMessage(
|
||||
fMsg,
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: fMsg.roomId,
|
||||
senderId: fMsg.userId,
|
||||
receiverId: fMsg.toUserId,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (gift.giftSourceUrl != null && gift.special != null) {
|
||||
if (scGiftHasFullScreenEffect(gift.special)) {
|
||||
if (SCGlobalConfig.isGiftSpecialEffects &&
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_giftFxLog(
|
||||
'local trigger player play '
|
||||
@ -1477,6 +1498,14 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'animationCount=$animationCount',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else if (SCGlobalConfig.isLowPerformanceDevice &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_enqueueLowPerformanceGiftCompensation(
|
||||
gift: gift,
|
||||
receiver: u.user,
|
||||
quantity: quantity,
|
||||
coins: coins,
|
||||
);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because visual effects disabled '
|
||||
@ -1503,6 +1532,71 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _enqueueLowPerformanceGiftCompensation({
|
||||
required SocialChatGiftRes gift,
|
||||
required SocialChatUserProfile? receiver,
|
||||
required int quantity,
|
||||
required num coins,
|
||||
}) {
|
||||
final sender = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
_enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage(
|
||||
type: 1,
|
||||
priority: 900,
|
||||
userId: sender?.id ?? "",
|
||||
toUserId: receiver?.id ?? "",
|
||||
userAvatarUrl: sender?.userAvatar ?? "",
|
||||
toUserAvatarUrl: receiver?.userAvatar ?? "",
|
||||
userName: sender?.userNickname ?? "",
|
||||
toUserName: receiver?.userNickname ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
),
|
||||
dedupKey: _giftFloatingDedupKey(
|
||||
roomId: roomId,
|
||||
senderId: sender?.id,
|
||||
receiverId: receiver?.id,
|
||||
giftId: gift.id,
|
||||
quantity: quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueGiftFloatingMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (SCGlobalConfig.isLowPerformanceDevice) {
|
||||
OverlayManager().addLowPerformanceCompensationMessage(
|
||||
message,
|
||||
dedupKey: dedupKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
OverlayManager().addMessage(message);
|
||||
}
|
||||
|
||||
String _giftFloatingDedupKey({
|
||||
required String? roomId,
|
||||
required String? senderId,
|
||||
required String? receiverId,
|
||||
required String? giftId,
|
||||
required int quantity,
|
||||
}) {
|
||||
return [
|
||||
"gift",
|
||||
roomId ?? "",
|
||||
senderId ?? "",
|
||||
receiverId ?? "",
|
||||
giftId ?? "",
|
||||
quantity,
|
||||
].join("|");
|
||||
}
|
||||
|
||||
bool _supportsComboFeedback(SocialChatGiftRes gift) {
|
||||
final giftTab = (gift.giftTab ?? '').trim();
|
||||
return giftTab == "LUCK" ||
|
||||
@ -1599,6 +1693,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
required int quantity,
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) async {
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
@ -1626,6 +1721,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||||
toUser: firstTargetUser,
|
||||
targetUserIds: targetUserIds,
|
||||
giftBatchId: giftBatchId,
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
type: SCRoomMsgType.luckGiftAnimOther,
|
||||
@ -1651,6 +1747,20 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
String _createGiftBatchId(_GiftSendRequest request) {
|
||||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||||
..sort();
|
||||
return [
|
||||
request.isLuckyGiftRequest ? 'lucky' : 'gift',
|
||||
request.roomId,
|
||||
request.gift.id ?? '',
|
||||
request.quantity,
|
||||
request.clickCount,
|
||||
sortedAcceptUserIds.join(','),
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
].join('|');
|
||||
}
|
||||
}
|
||||
|
||||
class CheckNumber extends StatelessWidget {
|
||||
|
||||
@ -120,7 +120,6 @@ class _HomeEventPageState extends State<HomeEventPage>
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print('ads:${homeBanners[index].toJson()}');
|
||||
SCBannerUtils.openBanner(homeBanners[index], context);
|
||||
},
|
||||
child: netImage(
|
||||
|
||||
@ -25,11 +25,23 @@ import '../../../index/main_route.dart';
|
||||
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
|
||||
const Color _kPartySkeletonShell = Color(0xFF0F3730);
|
||||
const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41);
|
||||
const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53);
|
||||
const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177);
|
||||
const Color _kPartySkeletonBorder = Color(0x66D8B57B);
|
||||
|
||||
class SCHomePartyPage extends StatefulWidget {
|
||||
const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53);
|
||||
const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177);
|
||||
const Color _kPartySkeletonBorder = Color(0x66D8B57B);
|
||||
|
||||
class _RoomCountryFilterOption {
|
||||
const _RoomCountryFilterOption({
|
||||
required this.key,
|
||||
required this.name,
|
||||
this.flagUrl,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String name;
|
||||
final String? flagUrl;
|
||||
}
|
||||
|
||||
class SCHomePartyPage extends StatefulWidget {
|
||||
const SCHomePartyPage({super.key});
|
||||
|
||||
@override
|
||||
@ -67,6 +79,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
bool _wasTickerModeEnabled = false;
|
||||
bool _hasCompletedInitialRoomLoad = false;
|
||||
DateTime? _lastRoomCounterHydrationAt;
|
||||
String? _selectedCountryKey;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@ -198,6 +211,9 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
|
||||
bool _sameRoom(SocialChatRoomRes previous, SocialChatRoomRes next) {
|
||||
return previous.id == next.id &&
|
||||
previous.countryCode == next.countryCode &&
|
||||
previous.countryName == next.countryName &&
|
||||
previous.nationalFlag == next.nationalFlag &&
|
||||
previous.roomName == next.roomName &&
|
||||
previous.roomCover == next.roomCover &&
|
||||
previous.roomGameIcon == next.roomGameIcon &&
|
||||
@ -229,6 +245,73 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<SocialChatRoomRes> _filteredRooms() {
|
||||
final selectedCountryKey = _selectedCountryKey;
|
||||
if ((selectedCountryKey ?? "").isEmpty) {
|
||||
return rooms;
|
||||
}
|
||||
return rooms
|
||||
.where((room) => _countryFilterKeyForRoom(room) == selectedCountryKey)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<_RoomCountryFilterOption> _roomCountryFilterOptions() {
|
||||
final options = <_RoomCountryFilterOption>[];
|
||||
final seenKeys = <String>{};
|
||||
for (final room in rooms) {
|
||||
final key = _countryFilterKeyForRoom(room);
|
||||
final name = _countryFilterNameForRoom(room);
|
||||
if (key == null || name == null || seenKeys.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
options.add(
|
||||
_RoomCountryFilterOption(
|
||||
key: key,
|
||||
name: name,
|
||||
flagUrl: _trimToNull(room.nationalFlag),
|
||||
),
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
bool _syncCountryFilterWithRooms() {
|
||||
final selectedCountryKey = _selectedCountryKey;
|
||||
if ((selectedCountryKey ?? "").isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final hasSelectedCountry = rooms.any(
|
||||
(room) => _countryFilterKeyForRoom(room) == selectedCountryKey,
|
||||
);
|
||||
if (hasSelectedCountry) {
|
||||
return false;
|
||||
}
|
||||
_selectedCountryKey = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
String? _countryFilterKeyForRoom(SocialChatRoomRes room) {
|
||||
final countryName = _trimToNull(room.countryName);
|
||||
if (countryName != null) {
|
||||
return countryName.toLowerCase();
|
||||
}
|
||||
final countryCode = _trimToNull(room.countryCode);
|
||||
return countryCode?.toUpperCase();
|
||||
}
|
||||
|
||||
String? _countryFilterNameForRoom(SocialChatRoomRes room) {
|
||||
return _trimToNull(room.countryName) ?? _trimToNull(room.countryCode);
|
||||
}
|
||||
|
||||
String? _trimToNull(String? value) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
Future<void> _syncVisibleRoomMemberCounts({bool force = false}) async {
|
||||
if (!mounted || rooms.isEmpty || _isHydratingVisibleRoomCounters) {
|
||||
return;
|
||||
@ -241,7 +324,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
return;
|
||||
}
|
||||
final targetRoomIds =
|
||||
rooms
|
||||
_filteredRooms()
|
||||
.take(_roomCounterHydrationLimit)
|
||||
.map((room) => room.id ?? "")
|
||||
.where((roomId) => roomId.trim().isNotEmpty)
|
||||
@ -323,6 +406,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
}
|
||||
setState(() {
|
||||
rooms = mergedRooms;
|
||||
_syncCountryFilterWithRooms();
|
||||
});
|
||||
unawaited(_syncVisibleRoomMemberCounts(force: true));
|
||||
} catch (_) {
|
||||
@ -623,24 +707,187 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData),
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
),
|
||||
padding: EdgeInsets.all(10),
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(rooms[index], index);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldShowLeaderboardSkeleton(SCAppGeneralManager ref) {
|
||||
final countryOptions = _roomCountryFilterOptions();
|
||||
final visibleRooms = _filteredRooms();
|
||||
final countrySelector = _buildCountryFilterSelector(countryOptions);
|
||||
if (visibleRooms.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
countrySelector,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
countrySelector,
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 8.w, 10.w, 10.w),
|
||||
itemCount: visibleRooms.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(visibleRooms[index], index);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryFilterSelector(
|
||||
List<_RoomCountryFilterOption> countryOptions,
|
||||
) {
|
||||
if (countryOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 8.w),
|
||||
child: SizedBox(
|
||||
height: 26.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
||||
itemCount: countryOptions.length + 1,
|
||||
separatorBuilder: (context, index) => SizedBox(width: 5.w),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return _buildCountryFilterChip(
|
||||
label: SCAppLocalizations.of(context)!.all,
|
||||
);
|
||||
}
|
||||
final option = countryOptions[index - 1];
|
||||
return Consumer<SCAppGeneralManager>(
|
||||
builder: (context, provider, child) {
|
||||
final country = provider.findCountryByName(option.name);
|
||||
final countryFlagUrl =
|
||||
_trimToNull(option.flagUrl) ??
|
||||
_trimToNull(country?.nationalFlag);
|
||||
final countryLabel =
|
||||
_trimToNull(country?.aliasName) ??
|
||||
_trimToNull(country?.countryName) ??
|
||||
option.name;
|
||||
return _buildCountryFilterChip(
|
||||
label: countryLabel,
|
||||
countryKey: option.key,
|
||||
flagUrl: countryFlagUrl,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryFilterChip({
|
||||
required String label,
|
||||
String? countryKey,
|
||||
String? flagUrl,
|
||||
}) {
|
||||
final isSelected = _selectedCountryKey == countryKey;
|
||||
final resolvedFlagUrl = _trimToNull(flagUrl);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_selectedCountryKey == countryKey) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedCountryKey = countryKey;
|
||||
});
|
||||
unawaited(_syncVisibleRoomMemberCounts(force: true));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
height: 24.w,
|
||||
constraints: BoxConstraints(minWidth: 45.w, maxWidth: 95.w),
|
||||
alignment: AlignmentDirectional.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors:
|
||||
isSelected
|
||||
? const [Color(0xFF5C4430), Color(0xFF231B14)]
|
||||
: [
|
||||
Colors.black.withValues(alpha: 0.34),
|
||||
const Color(0xFF0F3730).withValues(alpha: 0.38),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected
|
||||
? const Color(0xFFE8C681)
|
||||
: const Color(0x66D8B57B),
|
||||
width: 1.w,
|
||||
),
|
||||
boxShadow:
|
||||
isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFE8C681).withValues(alpha: 0.16),
|
||||
blurRadius: 5.w,
|
||||
offset: Offset(0, 2.w),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (resolvedFlagUrl != null) ...[
|
||||
netImage(
|
||||
url: resolvedFlagUrl,
|
||||
width: 15.w,
|
||||
height: 10.w,
|
||||
borderRadius: BorderRadius.circular(2.w),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: Directionality.of(context),
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected
|
||||
? const Color(0xFFFFE4A3)
|
||||
: Colors.white.withValues(alpha: 0.92),
|
||||
fontSize: 8.sp,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldShowLeaderboardSkeleton(SCAppGeneralManager ref) {
|
||||
return _isLeaderboardLoading && !_hasLeaderboardData(ref);
|
||||
}
|
||||
|
||||
@ -985,6 +1232,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
.loadPartyRooms(forceRefresh: forceRefresh)
|
||||
.then((values) {
|
||||
rooms = _mergeLatestRoomsWithCurrentCounters(values);
|
||||
_syncCountryFilterWithRooms();
|
||||
isLoading = false;
|
||||
_hasCompletedInitialRoomLoad = true;
|
||||
_refreshController.refreshCompleted();
|
||||
|
||||
@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/main.dart' show routeObserver;
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/modules/home/index_home_page.dart';
|
||||
import 'package:yumi/modules/chat/message/sc_message_page.dart';
|
||||
import 'package:yumi/modules/user/task/task_page.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -18,12 +20,12 @@ import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||||
import '../../shared/tools/sc_entry_popup_coordinator.dart';
|
||||
import '../../shared/tools/sc_lk_event_bus.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||||
import '../../ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart';
|
||||
import '../../ui_kit/widgets/register_reward/register_reward_dialog.dart';
|
||||
import '../home/popular/event/home_event_page.dart';
|
||||
import '../user/me_page2.dart';
|
||||
|
||||
/// 首页
|
||||
@ -34,29 +36,35 @@ class SCIndexPage extends StatefulWidget {
|
||||
State<SCIndexPage> createState() => _SCIndexPageState();
|
||||
}
|
||||
|
||||
class _SCIndexPageState extends State<SCIndexPage> {
|
||||
class _SCIndexPageState extends State<SCIndexPage>
|
||||
with WidgetsBindingObserver, RouteAware {
|
||||
static const Duration _registerRewardSocketWaitTimeout = Duration(seconds: 6);
|
||||
|
||||
int _currentIndex = 0;
|
||||
final List<Widget> _pages = [];
|
||||
final Set<int> _builtPageIndexes = <int>{0};
|
||||
final List<BottomNavigationBarItem> _bottomItems = [];
|
||||
final ValueNotifier<int> _taskClaimableCount = ValueNotifier<int>(0);
|
||||
SCAppGeneralManager? generalProvider;
|
||||
Locale? _lastLocale;
|
||||
bool _hasShownEntryDialogs = false;
|
||||
bool _hasShownRegisterRewardDialog = false;
|
||||
bool _isShowingDailySignInDialog = false;
|
||||
bool _showRegisterRewardAfterDailySignIn = false;
|
||||
bool _isOpeningFirstRegisterRoomGame = false;
|
||||
StreamSubscription<RegisterRewardGrantedEvent>? _registerRewardSubscription;
|
||||
Completer<void>? _registerRewardSocketCompleter;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_registerRewardSubscription = eventBus
|
||||
.on<RegisterRewardGrantedEvent>()
|
||||
.listen(_onRegisterRewardGranted);
|
||||
_initializePages();
|
||||
unawaited(_loadTaskClaimableCount());
|
||||
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
@ -66,9 +74,7 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).prewarmRtcEngine().catchError((error, stackTrace) {
|
||||
debugPrint('[Agora] prewarm on index failed: $error');
|
||||
}),
|
||||
).prewarmRtcEngine().catchError((error, stackTrace) {}),
|
||||
);
|
||||
Provider.of<RtmProvider>(context, listen: false).init(context);
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
@ -83,26 +89,89 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
WakelockPlus.enable();
|
||||
_showEntryDialogs();
|
||||
});
|
||||
String roomId = DataPersistence.getLastTimeRoomId();
|
||||
if (roomId.isNotEmpty) {
|
||||
SCAccountRepository().quitRoom(roomId).catchError((e) {
|
||||
return true; // 错误已处理
|
||||
});
|
||||
if (DataPersistence.getLastTimeRoomId().isNotEmpty) {
|
||||
unawaited(DataPersistence.setLastTimeRoomId(""));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
routeObserver.unsubscribe(this);
|
||||
OverlayManager().setHomeRootTabsVisible(false);
|
||||
_registerRewardSubscription?.cancel();
|
||||
_taskClaimableCount.dispose();
|
||||
WakelockPlus.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).syncRoomRedPacketBroadcastGroup(),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_showEntryPopupIfNeeded());
|
||||
});
|
||||
unawaited(_loadTaskClaimableCount());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
SCFloatIchart().init(context);
|
||||
_rebuildBottomItemsIfNeeded();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
|
||||
if (_routeObserverRoute != null) {
|
||||
routeObserver.unsubscribe(this);
|
||||
}
|
||||
_routeObserverRoute = route;
|
||||
routeObserver.subscribe(this, route);
|
||||
_setHomeRootTabsVisible(route.isCurrent);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
_setHomeRootTabsVisible(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
_setHomeRootTabsVisible(true);
|
||||
unawaited(_loadTaskClaimableCount());
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
_setHomeRootTabsVisible(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
_setHomeRootTabsVisible(false);
|
||||
}
|
||||
|
||||
void _setHomeRootTabsVisible(bool visible) {
|
||||
if (!mounted && visible) {
|
||||
return;
|
||||
}
|
||||
OverlayManager().setHomeRootTabsVisible(visible);
|
||||
if (visible) {
|
||||
unawaited(
|
||||
Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).syncRoomRedPacketBroadcastGroup(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -201,7 +270,12 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
|
||||
_pages.add(SCIndexHomePage());
|
||||
_pages.add(HomeEventPage());
|
||||
_pages.add(
|
||||
TaskPage(
|
||||
showBackButton: false,
|
||||
onTaskStatusChanged: () => unawaited(_loadTaskClaimableCount()),
|
||||
),
|
||||
);
|
||||
_pages.add(SCMessagePage());
|
||||
_pages.add(MePage2());
|
||||
}
|
||||
@ -222,6 +296,9 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
|
||||
void _switchBottomTab(int index) {
|
||||
if (index == 1) {
|
||||
unawaited(_loadTaskClaimableCount());
|
||||
}
|
||||
if (index == _currentIndex) {
|
||||
return;
|
||||
}
|
||||
@ -256,17 +333,9 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
);
|
||||
_bottomItems.add(
|
||||
BottomNavigationBarItem(
|
||||
icon: _buildBottomTabIcon(
|
||||
active: false,
|
||||
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_explore_no.png",
|
||||
),
|
||||
activeIcon: _buildBottomTabIcon(
|
||||
active: true,
|
||||
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_explore_en.png",
|
||||
),
|
||||
label: SCAppLocalizations.of(context)!.explore,
|
||||
icon: _buildTaskTabIcon(active: false),
|
||||
activeIcon: _buildTaskTabIcon(active: true),
|
||||
label: SCAppLocalizations.of(context)!.task,
|
||||
),
|
||||
);
|
||||
|
||||
@ -346,12 +415,22 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadTaskClaimableCount() async {
|
||||
try {
|
||||
final count = await SCAccountRepository().taskClaimableCount();
|
||||
if (!mounted) return;
|
||||
_taskClaimableCount.value = count;
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
_taskClaimableCount.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showEntryDialogs() async {
|
||||
if (_hasShownEntryDialogs || !mounted) {
|
||||
return;
|
||||
}
|
||||
_hasShownEntryDialogs = true;
|
||||
debugPrint('[SignInReward][Home] start entry dialog flow');
|
||||
|
||||
await _waitForRegisterRewardSocketIfNeeded();
|
||||
if (!mounted) {
|
||||
@ -363,19 +442,16 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
final dialogData = await _loadDailySignInDialogData();
|
||||
if (!mounted || dialogData == null) {
|
||||
debugPrint(
|
||||
'[SignInReward][Home] skip daily sign-in dialog reason=no-data',
|
||||
);
|
||||
await _showEntryPopupIfNeeded();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dialogData = await _loadDailySignInDialogData();
|
||||
if (!mounted || dialogData == null) {
|
||||
await _openFirstRegisterRoomGameIfNeeded();
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[SignInReward][Home] show daily sign-in dialog '
|
||||
'checkedToday=${dialogData.checkedToday} '
|
||||
'currentDay=${dialogData.currentItem?.day ?? 0} '
|
||||
'items=${dialogData.items.length}',
|
||||
);
|
||||
_isShowingDailySignInDialog = true;
|
||||
await DailySignInDialog.show(context, data: dialogData);
|
||||
_isShowingDailySignInDialog = false;
|
||||
@ -386,6 +462,20 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
_showRegisterRewardAfterDailySignIn = false;
|
||||
await _showRegisterRewardDialogIfNeeded();
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await _openFirstRegisterRoomGameIfNeeded();
|
||||
}
|
||||
|
||||
Future<void> _showEntryPopupIfNeeded() async {
|
||||
if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||
return;
|
||||
}
|
||||
await SCEntryPopupCoordinator.showIfNeeded(
|
||||
context,
|
||||
onSelectRootTab: _switchBottomTab,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
||||
@ -446,33 +536,39 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
|
||||
Future<DailySignInDialogData?> _loadDailySignInDialogData() async {
|
||||
try {
|
||||
debugPrint('[SignInReward][Home] loading status');
|
||||
final statusRes = await SCAccountRepository().signInRewardStatus();
|
||||
if (!statusRes.canShowDialog) {
|
||||
debugPrint(
|
||||
'[SignInReward][Home] dialog disabled '
|
||||
'configured=${statusRes.configured} '
|
||||
'enabled=${statusRes.enabled} '
|
||||
'available=${statusRes.available} '
|
||||
'signedToday=${statusRes.signedToday} '
|
||||
'days=${statusRes.days.length}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final dialogData = DailySignInDialogData.fromStatus(statusRes: statusRes);
|
||||
debugPrint(
|
||||
'[SignInReward][Home] mapped dialog data '
|
||||
'checkedToday=${dialogData.checkedToday} '
|
||||
'currentDay=${dialogData.currentItem?.day ?? 0} '
|
||||
'days=${dialogData.items.length}',
|
||||
);
|
||||
return dialogData;
|
||||
} catch (error) {
|
||||
debugPrint('[SignInReward][Home] load status failed error=$error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openFirstRegisterRoomGameIfNeeded() async {
|
||||
if (_isOpeningFirstRegisterRoomGame ||
|
||||
!DataPersistence.getPendingFirstRegisterRoomGameEvent()) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isOpeningFirstRegisterRoomGame = true;
|
||||
try {
|
||||
await DataPersistence.clearPendingFirstRegisterRoomGameEvent();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await SCEntryPopupCoordinator.openFirstPartyRoomRandomGame(context);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
_isOpeningFirstRegisterRoomGame = false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBottomTabIcon({
|
||||
required bool active,
|
||||
required String svgaPath,
|
||||
@ -487,4 +583,34 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskTabIcon({required bool active}) {
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _taskClaimableCount,
|
||||
builder: (_, claimableCount, __) {
|
||||
final icon = _buildBottomTabIcon(
|
||||
active: active,
|
||||
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
|
||||
fallbackPath:
|
||||
active
|
||||
? "sc_images/index/sc_icon_explore_en.png"
|
||||
: "sc_images/index/sc_icon_explore_no.png",
|
||||
);
|
||||
if (claimableCount <= 0) {
|
||||
return icon;
|
||||
}
|
||||
return Badge(
|
||||
backgroundColor: Colors.red,
|
||||
label: text(
|
||||
claimableCount > 99 ? "99+" : "$claimableCount",
|
||||
fontSize: 9.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: icon,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ import 'package:yumi/modules/admin/editing/sc_editing_user_room_page.dart';
|
||||
import 'package:yumi/modules/admin/search/sc_edit_room_search_admin_page.dart';
|
||||
import 'package:yumi/modules/admin/search/sc_edit_user_search_admin_page.dart';
|
||||
import 'package:yumi/modules/user/profile/person_detail_page.dart';
|
||||
import 'package:yumi/modules/user/profile/profile_gift_wall_detail_page.dart';
|
||||
import 'package:yumi/modules/user/profile/profile_wall_of_honors_page.dart';
|
||||
import 'package:yumi/modules/search/sc_search_page.dart';
|
||||
import 'package:yumi/modules/media/image_preview_page.dart';
|
||||
import 'package:yumi/modules/media/video_player_page.dart';
|
||||
@ -26,6 +28,8 @@ import '../user/edit/edit_user_info_page2.dart';
|
||||
class SCMainRoute implements SCIRouterProvider {
|
||||
static String report = '/main/report';
|
||||
static String person = '/main/person';
|
||||
static String giftWall = '/main/person/giftWall';
|
||||
static String wallOfHonors = '/main/person/wallOfHonors';
|
||||
static String edit = '/main/person/edit';
|
||||
static String follow = '/main/me/follow';
|
||||
static String vistors = '/main/me/vistors';
|
||||
@ -63,6 +67,22 @@ class SCMainRoute implements SCIRouterProvider {
|
||||
),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
giftWall,
|
||||
handler: Handler(
|
||||
handlerFunc:
|
||||
(_, params) =>
|
||||
ProfileGiftWallDetailPage(tageId: params['tageId']!.first),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
wallOfHonors,
|
||||
handler: Handler(
|
||||
handlerFunc:
|
||||
(_, params) =>
|
||||
ProfileWallOfHonorsPage(tageId: params['tageId']!.first),
|
||||
),
|
||||
);
|
||||
router.define(
|
||||
mainSearch,
|
||||
handler: Handler(handlerFunc: (_, params) => SearchPage()),
|
||||
@ -143,7 +163,6 @@ class SCMainRoute implements SCIRouterProvider {
|
||||
initialIndex = int.tryParse(params['initialIndex']!.first) ?? 0;
|
||||
}
|
||||
} catch (e) {
|
||||
print('解析图片预览参数失败: $e');
|
||||
// 可以返回错误页面或默认页面
|
||||
}
|
||||
|
||||
|
||||
108
lib/modules/room/background/room_background_apply_service.dart
Normal file
108
lib/modules/room/background/room_background_apply_service.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_history.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
|
||||
class RoomBackgroundApplyService {
|
||||
static Future<String> apply(BuildContext context, String sourcePath) async {
|
||||
final normalizedSource = sourcePath.trim();
|
||||
if (normalizedSource.isEmpty) {
|
||||
throw StateError("Room background source is empty");
|
||||
}
|
||||
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
final rtmProvider = context.read<RtmProvider>();
|
||||
final roomManager = context.read<SocialChatRoomManager>();
|
||||
final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile;
|
||||
final roomId = (roomProfile?.id ?? "").trim();
|
||||
final roomAccount = (roomProfile?.roomAccount ?? "").trim();
|
||||
if (roomId.isEmpty) {
|
||||
throw StateError("Room id is empty");
|
||||
}
|
||||
|
||||
final uploadedUrl =
|
||||
_isNetworkImage(normalizedSource)
|
||||
? normalizedSource
|
||||
: await _uploadSource(normalizedSource);
|
||||
if (uploadedUrl.trim().isEmpty) {
|
||||
throw StateError("Uploaded background URL is empty");
|
||||
}
|
||||
|
||||
final roomInfo = await SCAccountRepository().updateRoomBackground(
|
||||
roomId,
|
||||
uploadedUrl,
|
||||
);
|
||||
final roomBackground = _preferNonEmpty(
|
||||
roomInfo.roomBackground,
|
||||
uploadedUrl,
|
||||
);
|
||||
rtcProvider.updateCurrentRoomBasicInfo(roomBackground: roomBackground);
|
||||
roomManager.updateMyRoomInfo(
|
||||
roomInfo.copyWith(
|
||||
id: roomInfo.id ?? roomId,
|
||||
roomBackground: roomBackground,
|
||||
),
|
||||
);
|
||||
if (roomAccount.isNotEmpty) {
|
||||
rtmProvider.dispatchMessage(
|
||||
Msg(
|
||||
groupId: roomAccount,
|
||||
msg: roomInfo.id ?? roomId,
|
||||
type: SCRoomMsgType.roomSettingUpdate,
|
||||
),
|
||||
addLocal: false,
|
||||
);
|
||||
}
|
||||
await RoomBackgroundHistory.add(roomBackground);
|
||||
return roomBackground;
|
||||
}
|
||||
|
||||
static bool _isNetworkImage(String value) {
|
||||
return value.startsWith("http://") || value.startsWith("https://");
|
||||
}
|
||||
|
||||
static Future<String> _uploadSource(String sourcePath) async {
|
||||
final sourceFile =
|
||||
_isAssetPath(sourcePath)
|
||||
? await _copyAssetToTemporaryFile(sourcePath)
|
||||
: File(sourcePath);
|
||||
if (!sourceFile.existsSync()) {
|
||||
throw StateError("Room background source file does not exist");
|
||||
}
|
||||
final uploadedUrl = await SCGeneralRepositoryImp().upload(sourceFile);
|
||||
return uploadedUrl;
|
||||
}
|
||||
|
||||
static bool _isAssetPath(String value) {
|
||||
return value.startsWith("assets/") || value.startsWith("sc_images/");
|
||||
}
|
||||
|
||||
static Future<File> _copyAssetToTemporaryFile(String assetPath) async {
|
||||
final bytes = await rootBundle.load(assetPath);
|
||||
final directory = await getTemporaryDirectory();
|
||||
final fileName = assetPath.split("/").last;
|
||||
final file = File("${directory.path}/room_background_$fileName");
|
||||
await file.writeAsBytes(
|
||||
bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
|
||||
flush: true,
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
static String _preferNonEmpty(String? primary, String fallback) {
|
||||
if ((primary ?? "").trim().isNotEmpty) {
|
||||
return primary!.trim();
|
||||
}
|
||||
return fallback.trim();
|
||||
}
|
||||
}
|
||||
14
lib/modules/room/background/room_background_assets.dart
Normal file
14
lib/modules/room/background/room_background_assets.dart
Normal file
@ -0,0 +1,14 @@
|
||||
const List<String> roomBackgroundExampleAssets = [
|
||||
"sc_images/room/background_examples/bg_example_1.png",
|
||||
"sc_images/room/background_examples/bg_example_2.png",
|
||||
"sc_images/room/background_examples/bg_example_3.png",
|
||||
"sc_images/room/background_examples/bg_example_4.png",
|
||||
"sc_images/room/background_examples/bg_example_5.png",
|
||||
"sc_images/room/background_examples/bg_example_6.png",
|
||||
"sc_images/room/background_examples/bg_example_7.png",
|
||||
"sc_images/room/background_examples/bg_example_8.png",
|
||||
];
|
||||
|
||||
bool isRoomBackgroundExampleAsset(String value) {
|
||||
return roomBackgroundExampleAssets.contains(value.trim());
|
||||
}
|
||||
48
lib/modules/room/background/room_background_history.dart
Normal file
48
lib/modules/room/background/room_background_history.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
|
||||
class RoomBackgroundHistory {
|
||||
static const int _maxCount = 30;
|
||||
static const String _prefix = "room_background_history";
|
||||
|
||||
static String get _key {
|
||||
final userId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "guest";
|
||||
return "${_prefix}_$userId";
|
||||
}
|
||||
|
||||
static List<String> load() {
|
||||
final raw = DataPersistence.getString(_key);
|
||||
if (raw.trim().isEmpty) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is List) {
|
||||
return decoded
|
||||
.map((item) => item?.toString().trim() ?? "")
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<void> add(String imageUrl) async {
|
||||
final normalized = imageUrl.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final next =
|
||||
load()
|
||||
..removeWhere((item) => item == normalized)
|
||||
..insert(0, normalized);
|
||||
if (next.length > _maxCount) {
|
||||
next.removeRange(_maxCount, next.length);
|
||||
}
|
||||
await DataPersistence.setString(_key, jsonEncode(next));
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,17 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_apply_service.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_assets.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_history.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
@ -30,13 +37,32 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController.addListener(_handleTabChange);
|
||||
_officialItems = [
|
||||
_RoomBackgroundItem(inUse: true),
|
||||
_RoomBackgroundItem(),
|
||||
_RoomBackgroundItem(),
|
||||
_RoomBackgroundItem(),
|
||||
];
|
||||
_mineItems = [_RoomBackgroundItem(), _RoomBackgroundItem()];
|
||||
final currentBackground = _currentRoomBackground();
|
||||
_officialItems =
|
||||
roomBackgroundExampleAssets
|
||||
.map(
|
||||
(path) => _RoomBackgroundItem(
|
||||
imagePath: path,
|
||||
inUse: path == currentBackground,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final historyItems = RoomBackgroundHistory.load();
|
||||
if (currentBackground.isNotEmpty &&
|
||||
!isRoomBackgroundExampleAsset(currentBackground) &&
|
||||
!historyItems.contains(currentBackground)) {
|
||||
historyItems.insert(0, currentBackground);
|
||||
RoomBackgroundHistory.add(currentBackground);
|
||||
}
|
||||
_mineItems =
|
||||
historyItems
|
||||
.map(
|
||||
(path) => _RoomBackgroundItem(
|
||||
imagePath: path,
|
||||
inUse: path == currentBackground,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -62,35 +88,81 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
}
|
||||
|
||||
setState(() {
|
||||
for (final item in _mineItems) {
|
||||
item.inUse = false;
|
||||
}
|
||||
_mineItems.insert(
|
||||
0,
|
||||
_RoomBackgroundItem(localImagePath: uploadedPath, inUse: true),
|
||||
);
|
||||
_markInUse(uploadedPath!);
|
||||
_upsertMineItem(uploadedPath, inUse: true);
|
||||
});
|
||||
_tabController.animateTo(1);
|
||||
}
|
||||
|
||||
void _selectItem(List<_RoomBackgroundItem> items, int index) {
|
||||
setState(() {
|
||||
for (final item in items) {
|
||||
item.inUse = false;
|
||||
}
|
||||
items[index].inUse = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _previewItem(List<_RoomBackgroundItem> items, int index) async {
|
||||
final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview<bool>(
|
||||
context,
|
||||
backgroundPath: items[index].localImagePath,
|
||||
backgroundPath: items[index].imagePath,
|
||||
);
|
||||
if (!mounted || shouldUse != true) {
|
||||
return;
|
||||
}
|
||||
_selectItem(items, index);
|
||||
await _applyItem(items, index);
|
||||
}
|
||||
|
||||
Future<void> _applyItem(List<_RoomBackgroundItem> items, int index) async {
|
||||
final item = items[index];
|
||||
final imagePath = item.imagePath.trim();
|
||||
if (imagePath.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
final appliedUrl = await RoomBackgroundApplyService.apply(
|
||||
context,
|
||||
imagePath,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_markInUse(imagePath);
|
||||
_upsertMineItem(appliedUrl, inUse: items == _mineItems);
|
||||
});
|
||||
SCTts.show(localizations.operationSuccessful);
|
||||
} catch (e, stackTrace) {
|
||||
if (mounted) {
|
||||
SCTts.show(localizations.operationFail);
|
||||
}
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
String _currentRoomBackground() {
|
||||
return context
|
||||
.read<RtcProvider>()
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomBackground
|
||||
?.trim() ??
|
||||
"";
|
||||
}
|
||||
|
||||
void _markInUse(String imagePath) {
|
||||
final normalized = imagePath.trim();
|
||||
for (final item in [..._officialItems, ..._mineItems]) {
|
||||
item.inUse = item.imagePath == normalized;
|
||||
}
|
||||
}
|
||||
|
||||
void _upsertMineItem(String imagePath, {bool inUse = false}) {
|
||||
final normalized = imagePath.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_mineItems.removeWhere((item) => item.imagePath == normalized);
|
||||
_mineItems.insert(0, _RoomBackgroundItem(imagePath: normalized));
|
||||
if (inUse) {
|
||||
_markInUse(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -371,13 +443,32 @@ class _RoomBackgroundCard extends StatelessWidget {
|
||||
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
if ((item.localImagePath ?? "").isNotEmpty) {
|
||||
final file = File(item.localImagePath!);
|
||||
final imagePath = item.imagePath.trim();
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
return Image.network(
|
||||
imagePath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations),
|
||||
);
|
||||
}
|
||||
if (imagePath.startsWith("assets/") || imagePath.startsWith("sc_images/")) {
|
||||
return Image.asset(
|
||||
imagePath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations),
|
||||
);
|
||||
}
|
||||
if (imagePath.isNotEmpty) {
|
||||
final file = File(imagePath);
|
||||
if (file.existsSync()) {
|
||||
return Image.file(file, fit: BoxFit.cover);
|
||||
}
|
||||
}
|
||||
|
||||
return _buildEmptyPreview(localizations);
|
||||
}
|
||||
|
||||
Widget _buildEmptyPreview(SCAppLocalizations localizations) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@ -416,8 +507,8 @@ class _RoomBackgroundCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _RoomBackgroundItem {
|
||||
_RoomBackgroundItem({this.localImagePath, this.inUse = false});
|
||||
_RoomBackgroundItem({required this.imagePath, this.inUse = false});
|
||||
|
||||
final String? localImagePath;
|
||||
final String imagePath;
|
||||
bool inUse;
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_apply_service.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_assets.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
@ -20,8 +24,12 @@ class RoomBackgroundUploadPage extends StatefulWidget {
|
||||
class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
File? _selectedImage;
|
||||
bool _isSaving = false;
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
if (_isSaving) {
|
||||
return;
|
||||
}
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 90,
|
||||
@ -37,6 +45,40 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveBackground() async {
|
||||
final selectedImage = _selectedImage;
|
||||
if (selectedImage == null || _isSaving) {
|
||||
return;
|
||||
}
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
final roomBackground = await RoomBackgroundApplyService.apply(
|
||||
context,
|
||||
selectedImage.path,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SCTts.show(localizations.operationSuccessful);
|
||||
SCNavigatorUtils.goBackWithParams(context, roomBackground);
|
||||
} catch (e, stackTrace) {
|
||||
if (mounted) {
|
||||
SCTts.show(localizations.operationFail);
|
||||
}
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
@ -125,12 +167,18 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _RoomBackgroundExampleCard()),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(child: _RoomBackgroundExampleCard()),
|
||||
],
|
||||
SizedBox(
|
||||
height: 300.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 28.w),
|
||||
itemCount: roomBackgroundExampleAssets.length,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16.w),
|
||||
itemBuilder:
|
||||
(context, index) => _RoomBackgroundExampleCard(
|
||||
assetPath: roomBackgroundExampleAssets[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
@ -144,15 +192,9 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
SizedBox(height: 28.w),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap:
|
||||
_selectedImage == null
|
||||
? null
|
||||
: () => SCNavigatorUtils.goBackWithParams(
|
||||
context,
|
||||
_selectedImage!.path,
|
||||
),
|
||||
onTap: _selectedImage == null ? null : _saveBackground,
|
||||
child: Opacity(
|
||||
opacity: _selectedImage == null ? 0.45 : 1,
|
||||
opacity: _selectedImage == null || _isSaving ? 0.45 : 1,
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
width: double.infinity,
|
||||
@ -188,50 +230,63 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
}
|
||||
|
||||
class _RoomBackgroundExampleCard extends StatelessWidget {
|
||||
const _RoomBackgroundExampleCard({required this.assetPath});
|
||||
|
||||
final String assetPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Container(
|
||||
return SizedBox(
|
||||
width: 140.w,
|
||||
height: 300.w,
|
||||
decoration: BoxDecoration(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14.w),
|
||||
color: const Color(0xff143B34),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 42.w,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 12.w,
|
||||
right: 12.w,
|
||||
bottom: 18.w,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
border: Border.all(
|
||||
color: SocialChatTheme.primaryLight,
|
||||
width: 1.w,
|
||||
),
|
||||
color: const Color(0xff1B4A40),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
assetPath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => Container(
|
||||
color: const Color(0xff143B34),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 42.w,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
localizations.staticOrGifImage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
Positioned(
|
||||
left: 12.w,
|
||||
right: 12.w,
|
||||
bottom: 18.w,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
border: Border.all(
|
||||
color: SocialChatTheme.primaryLight,
|
||||
width: 1.w,
|
||||
),
|
||||
color: const Color(0xff1B4A40).withValues(alpha: 0.82),
|
||||
),
|
||||
child: Text(
|
||||
localizations.staticOrGifImage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@ class RoomEditPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomEditPageState extends State<RoomEditPage> {
|
||||
static const Set<int> _roomGroupReadyCodes = {0, 10021, 10025};
|
||||
|
||||
RtcProvider? rtcProvider;
|
||||
bool isEdit = false;
|
||||
final TextEditingController _roomNameController = TextEditingController();
|
||||
@ -181,9 +183,6 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
bool success,
|
||||
String url,
|
||||
) {
|
||||
debugPrint(
|
||||
"[Room Cover] upload callback success=$success url=$url",
|
||||
);
|
||||
if (success) {
|
||||
setState(() {
|
||||
roomCover = url;
|
||||
@ -528,9 +527,6 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.show(context: context);
|
||||
debugPrint(
|
||||
"[Room Cover] submit roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""} roomCover=$submittedRoomCover roomName=$submittedRoomName roomDesc=$submittedRoomDesc",
|
||||
);
|
||||
var roomInfo = await SCAccountRepository().editRoomInfo(
|
||||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
submittedRoomCover,
|
||||
@ -538,7 +534,6 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
submittedRoomDesc,
|
||||
SCRoomInfoEventType.AVAILABLE.name,
|
||||
);
|
||||
debugPrint("[Room Cover] editRoomInfo response: ${roomInfo.toJson()}");
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
@ -555,9 +550,6 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
);
|
||||
roomManager.updateMyRoomInfo(mergedRoomInfo);
|
||||
if (widget.needRestCurrentRoomInfo != "true") {
|
||||
debugPrint(
|
||||
"[Room Cover Sync] dispatch roomSettingUpdate groupId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""} roomId=${mergedRoomInfo.id ?? rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""}",
|
||||
);
|
||||
currentRtmProvider.dispatchMessage(
|
||||
Msg(
|
||||
groupId:
|
||||
@ -582,7 +574,7 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
if (c.code == 0) {
|
||||
if (_roomGroupReadyCodes.contains(c.code)) {
|
||||
SCLoadingManager.hide();
|
||||
SCNavigatorUtils.goBack(context);
|
||||
currentRtcProvider.joinVoiceRoomSession(
|
||||
|
||||
@ -9,10 +9,10 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
|
||||
@ -94,10 +94,10 @@ class _RoomMemberPageState
|
||||
list.isNotEmpty ? buildTag(list.first.roles) : Container(),
|
||||
SizedBox(height: 3.w),
|
||||
Column(
|
||||
children: List.generate(list.length, (cIndex) {
|
||||
SocialChatRoomMemberRes userInfo = list[cIndex];
|
||||
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||
return GestureDetector(
|
||||
children: List.generate(list.length, (cIndex) {
|
||||
SocialChatRoomMemberRes userInfo = list[cIndex];
|
||||
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
||||
@ -108,9 +108,10 @@ class _RoomMemberPageState
|
||||
child: Row(
|
||||
children: [
|
||||
head(
|
||||
url: userProfile?.userAvatar ?? "",
|
||||
width: 52.w,
|
||||
headdress: userProfile?.getHeaddress()?.sourceUrl,
|
||||
url: userProfile?.userAvatar ?? "",
|
||||
width: 52.w,
|
||||
headdress: userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Column(
|
||||
@ -130,47 +131,44 @@ class _RoomMemberPageState
|
||||
SizedBox(width: 3.w),
|
||||
socialchatNickNameText(
|
||||
maxWidth: 160.w,
|
||||
userProfile?.userNickname ?? "",
|
||||
userProfile?.userNickname ?? "",
|
||||
fontSize: 14.sp,
|
||||
textColor: Colors.black,
|
||||
type:
|
||||
userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(userProfile
|
||||
?.userNickname
|
||||
?.characters
|
||||
.length ??
|
||||
0) >
|
||||
type: userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(userProfile
|
||||
?.userNickname
|
||||
?.characters
|
||||
.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: userProfile?.getVIP()?.cover ?? "",
|
||||
userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 5.w),
|
||||
userProfile?.wearBadge?.isNotEmpty ??
|
||||
false
|
||||
userProfile?.wearBadge?.isNotEmpty ?? false
|
||||
? SizedBox(
|
||||
height: 25.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
itemCount:
|
||||
userProfile?.wearBadge?.length ??
|
||||
0,
|
||||
userProfile?.wearBadge?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
return netImage(
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
url:
|
||||
userProfile
|
||||
?.wearBadge?[index]
|
||||
.selectUrl ??
|
||||
"",
|
||||
userProfile
|
||||
?.wearBadge?[index]
|
||||
.selectUrl ??
|
||||
"",
|
||||
);
|
||||
},
|
||||
separatorBuilder: (
|
||||
@ -188,17 +186,16 @@ class _RoomMemberPageState
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SCSpecialIdBadge(
|
||||
idText: userProfile?.getID() ?? "",
|
||||
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||
idText: userProfile?.getID() ?? "",
|
||||
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||
assetPath: SCSpecialIdAssets.userId,
|
||||
animationWidth: 62.w,
|
||||
animationHeight: 24.w,
|
||||
showTextBesideAnimated: true,
|
||||
animatedTextSpacing: 0,
|
||||
showAnimatedGradientText:
|
||||
userProfile
|
||||
?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
userProfile?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
animationTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
@ -352,29 +349,27 @@ class _RoomMemberPageState
|
||||
if (onErr != null) {
|
||||
onErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _mergeCurrentUserVip(
|
||||
SocialChatUserProfile? profile,
|
||||
) {
|
||||
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||
return profile;
|
||||
}
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentProfile == null ||
|
||||
currentProfile.vipLevelForColoredId <= 0 ||
|
||||
currentProfile.id != profile.id) {
|
||||
return profile;
|
||||
}
|
||||
return profile.copyWith(
|
||||
vipLevel:
|
||||
currentProfile.vipLevel ??
|
||||
currentProfile.vipLevelForColoredId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTag(String? roles) {
|
||||
}
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _mergeCurrentUserVip(SocialChatUserProfile? profile) {
|
||||
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||
return profile;
|
||||
}
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentProfile == null ||
|
||||
currentProfile.vipLevelForColoredId <= 0 ||
|
||||
currentProfile.id != profile.id) {
|
||||
return profile;
|
||||
}
|
||||
return profile.copyWith(
|
||||
vipLevel:
|
||||
currentProfile.vipLevel ??
|
||||
currentProfile.vipLevelForColoredId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTag(String? roles) {
|
||||
if (roles == SCRoomRolesType.HOMEOWNER.name) {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@ -101,6 +101,7 @@ class _RoomOnlinePageState
|
||||
url: userInfo.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: userInfo.getHeaddress()?.sourceUrl,
|
||||
headdressCover: userInfo.getHeaddress()?.cover,
|
||||
),
|
||||
onTap: () {
|
||||
_openUserCard(userInfo);
|
||||
|
||||
@ -10,7 +10,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart';
|
||||
|
||||
///麦位
|
||||
@ -97,6 +96,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
width: seatAvatarSize,
|
||||
height: seatAvatarSize,
|
||||
headdress: resolvedHeaddress,
|
||||
headdressCover: seatSnapshot.headdressCoverUrl,
|
||||
),
|
||||
)
|
||||
: (seatSnapshot.micLock
|
||||
@ -114,16 +114,8 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||
left: widget.isGameModel ? 2.w : 5.w,
|
||||
child:
|
||||
seatSnapshot.isCurrentUser
|
||||
? Selector<RoomMusicManager, bool>(
|
||||
selector:
|
||||
(_, manager) => manager.isPublishingToRoom,
|
||||
builder: (context, isPlayingBgm, child) {
|
||||
return isPlayingBgm
|
||||
? const _BgmPlayingIndicator()
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
seatSnapshot.isBgmPlaying
|
||||
? const _BgmPlayingIndicator()
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
@ -327,6 +319,7 @@ class _SeatRenderSnapshot {
|
||||
required this.userId,
|
||||
required this.userAvatar,
|
||||
required this.headdressSourceUrl,
|
||||
required this.headdressCoverUrl,
|
||||
required this.userNickname,
|
||||
required this.userRoles,
|
||||
required this.userVipName,
|
||||
@ -335,6 +328,7 @@ class _SeatRenderSnapshot {
|
||||
required this.micMute,
|
||||
required this.soundWaveEnabled,
|
||||
required this.isCurrentUser,
|
||||
required this.isBgmPlaying,
|
||||
});
|
||||
|
||||
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
|
||||
@ -349,6 +343,7 @@ class _SeatRenderSnapshot {
|
||||
userId: user?.id ?? "",
|
||||
userAvatar: user?.userAvatar ?? "",
|
||||
headdressSourceUrl: user?.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCoverUrl: user?.getHeaddress()?.cover ?? "",
|
||||
userNickname: user?.userNickname ?? "",
|
||||
userRoles: user?.roles ?? "",
|
||||
userVipName: user?.getVIP()?.name ?? "",
|
||||
@ -357,6 +352,7 @@ class _SeatRenderSnapshot {
|
||||
micMute: roomSeat?.micMute ?? false,
|
||||
soundWaveEnabled: provider.shouldShowSeatSoundWave(index),
|
||||
isCurrentUser: provider.isOnMaiInIndex(index),
|
||||
isBgmPlaying: provider.isUserPublishingRoomMusic(user?.id),
|
||||
);
|
||||
}
|
||||
|
||||
@ -365,6 +361,7 @@ class _SeatRenderSnapshot {
|
||||
final String userId;
|
||||
final String userAvatar;
|
||||
final String headdressSourceUrl;
|
||||
final String headdressCoverUrl;
|
||||
final String userNickname;
|
||||
final String userRoles;
|
||||
final String userVipName;
|
||||
@ -373,6 +370,7 @@ class _SeatRenderSnapshot {
|
||||
final bool micMute;
|
||||
final bool soundWaveEnabled;
|
||||
final bool isCurrentUser;
|
||||
final bool isBgmPlaying;
|
||||
|
||||
bool get hasUser => userId.isNotEmpty;
|
||||
|
||||
@ -388,6 +386,7 @@ class _SeatRenderSnapshot {
|
||||
other.userId == userId &&
|
||||
other.userAvatar == userAvatar &&
|
||||
other.headdressSourceUrl == headdressSourceUrl &&
|
||||
other.headdressCoverUrl == headdressCoverUrl &&
|
||||
other.userNickname == userNickname &&
|
||||
other.userRoles == userRoles &&
|
||||
other.userVipName == userVipName &&
|
||||
@ -395,7 +394,8 @@ class _SeatRenderSnapshot {
|
||||
other.micLock == micLock &&
|
||||
other.micMute == micMute &&
|
||||
other.soundWaveEnabled == soundWaveEnabled &&
|
||||
other.isCurrentUser == isCurrentUser;
|
||||
other.isCurrentUser == isCurrentUser &&
|
||||
other.isBgmPlaying == isBgmPlaying;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -405,6 +405,7 @@ class _SeatRenderSnapshot {
|
||||
userId,
|
||||
userAvatar,
|
||||
headdressSourceUrl,
|
||||
headdressCoverUrl,
|
||||
userNickname,
|
||||
userRoles,
|
||||
userVipName,
|
||||
@ -413,6 +414,7 @@ class _SeatRenderSnapshot {
|
||||
micMute,
|
||||
soundWaveEnabled,
|
||||
isCurrentUser,
|
||||
isBgmPlaying,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import 'package:yumi/services/gift/gift_system_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_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_lucky_gift_win_sound_player.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
@ -61,6 +62,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
static const Duration _giftFlightBatchDedupeWindow = Duration(
|
||||
milliseconds: 650,
|
||||
);
|
||||
static const Duration _giftVisualBatchDedupeWindow = Duration(seconds: 4);
|
||||
|
||||
late TabController _tabController;
|
||||
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
||||
@ -70,6 +72,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
|
||||
<String, _LuckyGiftComboSession>{};
|
||||
final Map<String, Timer> _giftFlightBatchDedupeTimers = <String, Timer>{};
|
||||
final Map<String, Timer> _giftVisualBatchDedupeTimers = <String, Timer>{};
|
||||
int _shownRoomStartupFailureToken = 0;
|
||||
RtcProvider? _rtcProvider;
|
||||
bool _roomProviderRebuildScheduled = false;
|
||||
@ -91,6 +94,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
).toggleGiftAnimationVisibility(false);
|
||||
_clearLuckyGiftComboSessions();
|
||||
_clearGiftFlightBatchDedupeTimers();
|
||||
_clearGiftVisualBatchDedupeTimers();
|
||||
_giftSeatFlightController.clear();
|
||||
}
|
||||
});
|
||||
@ -104,6 +108,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_rtcProvider?.removeListener(_handleRoomProviderChanged);
|
||||
_rtcProvider = rtcProvider;
|
||||
rtcProvider.addListener(_handleRoomProviderChanged);
|
||||
context.read<RoomMusicManager>().attachRtcProvider(rtcProvider);
|
||||
}
|
||||
_ensureRoomVisualEffectsEnabled();
|
||||
}
|
||||
@ -177,6 +182,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
RoomEntranceHelper.clearQueue();
|
||||
_clearLuckyGiftComboSessions();
|
||||
_clearGiftFlightBatchDedupeTimers();
|
||||
_clearGiftVisualBatchDedupeTimers();
|
||||
_giftSeatFlightController.clear();
|
||||
OverlayManager().removeRoom();
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
|
||||
@ -252,7 +258,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
}
|
||||
}
|
||||
|
||||
String? _resolveRoomThemeBackground(JoinRoomRes? room) {
|
||||
String? _resolveRoomBackground(JoinRoomRes? room) {
|
||||
final roomBackground =
|
||||
room?.roomProfile?.roomProfile?.roomBackground?.trim() ?? "";
|
||||
if (roomBackground.isNotEmpty) {
|
||||
return roomBackground;
|
||||
}
|
||||
final roomTheme = room?.roomProps?.roomTheme;
|
||||
final themeBack = roomTheme?.themeBack ?? "";
|
||||
if (themeBack.isEmpty) {
|
||||
@ -291,11 +302,11 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
Selector<RtcProvider, String?>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_resolveRoomThemeBackground(provider.currenRoom),
|
||||
builder: (context, roomThemeBackground, child) {
|
||||
return roomThemeBackground != null
|
||||
_resolveRoomBackground(provider.currenRoom),
|
||||
builder: (context, roomBackground, child) {
|
||||
return roomBackground != null
|
||||
? netImage(
|
||||
url: roomThemeBackground,
|
||||
url: roomBackground,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight,
|
||||
noDefaultImg: true,
|
||||
@ -327,7 +338,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
const RoomBottomWidget(showGiftComboButton: false),
|
||||
],
|
||||
),
|
||||
LGiftAnimalPage(),
|
||||
PositionedDirectional(
|
||||
start: 0,
|
||||
end: 0,
|
||||
top: 0,
|
||||
bottom: RoomBottomWidget.floatingButtonHostHeight.w,
|
||||
child: const IgnorePointer(child: LGiftAnimalPage()),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -20),
|
||||
child: RoomAnimationQueueScreen(),
|
||||
@ -475,36 +492,47 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
||||
return;
|
||||
}
|
||||
var giftModel = LGiftModel();
|
||||
giftModel.labelId = "${msg.gift?.id}${msg.user?.id}${msg.toUser?.id}";
|
||||
giftModel.sendUserName = msg.user?.userNickname ?? "";
|
||||
giftModel.sendToUserName = msg.toUser?.userNickname ?? "";
|
||||
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
|
||||
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
|
||||
giftModel.giftCount = msg.number ?? 0;
|
||||
giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg);
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
giftModel.sendUserPic,
|
||||
logicalWidth: 26.w,
|
||||
logicalHeight: 26.w,
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
giftModel.giftPic,
|
||||
logicalWidth: 34.w,
|
||||
logicalHeight: 34.w,
|
||||
),
|
||||
);
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
|
||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
||||
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
|
||||
final shouldPlayBatchVisuals =
|
||||
targetUserIds.length <= 1 ||
|
||||
targetGroupKey == null ||
|
||||
_markGiftVisualBatchForPlayback(msg, targetGroupKey);
|
||||
if (shouldPlayBatchVisuals) {
|
||||
var giftModel = LGiftModel();
|
||||
giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey);
|
||||
giftModel.sendUserName = msg.user?.userNickname ?? "";
|
||||
giftModel.sendToUserName = _resolveGiftTickerTargetName(
|
||||
msg,
|
||||
targetUserIds,
|
||||
);
|
||||
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
|
||||
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
|
||||
giftModel.giftCount = msg.number ?? 0;
|
||||
giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg);
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
giftModel.sendUserPic,
|
||||
logicalWidth: 26.w,
|
||||
logicalHeight: 26.w,
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
giftModel.giftPic,
|
||||
logicalWidth: 34.w,
|
||||
logicalHeight: 34.w,
|
||||
),
|
||||
);
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||
if (_supportsComboMilestoneEffects(msg)) {
|
||||
_handleComboMilestoneVisuals(msg, targetGroupKey);
|
||||
}
|
||||
@ -569,6 +597,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context,
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
unawaited(
|
||||
SCLuckyGiftWinSoundPlayer.play(
|
||||
eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey(
|
||||
giftId: msg.gift?.id,
|
||||
userId: msg.user?.id,
|
||||
toUserId: msg.toUser?.id,
|
||||
awardAmount: awardAmount,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLuckyRewardAmount(num awardAmount) {
|
||||
@ -829,6 +867,22 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
String _buildGiftTickerLabelId(Msg msg, String? targetGroupKey) {
|
||||
final giftId = (msg.gift?.id ?? "").trim();
|
||||
final senderId = (msg.user?.id ?? "").trim();
|
||||
if ((targetGroupKey ?? "").trim().isNotEmpty) {
|
||||
return "$giftId|$senderId|$targetGroupKey";
|
||||
}
|
||||
return "$giftId|$senderId|${msg.toUser?.id ?? ""}";
|
||||
}
|
||||
|
||||
String _resolveGiftTickerTargetName(Msg msg, List<String> targetUserIds) {
|
||||
if (targetUserIds.length > 1) {
|
||||
return SCAppLocalizations.of(context)?.multiple ?? "Multiple";
|
||||
}
|
||||
return msg.toUser?.userNickname ?? "";
|
||||
}
|
||||
|
||||
List<String> _normalizeGiftTargetUserIds(List<String> targetUserIds) {
|
||||
final normalizedTargetUserIds = <String>[];
|
||||
for (final targetUserId in targetUserIds) {
|
||||
@ -851,6 +905,51 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
return normalizedTargetUserIds.join(",");
|
||||
}
|
||||
|
||||
bool _markGiftVisualBatchForPlayback(Msg msg, String targetGroupKey) {
|
||||
final batchKey = _buildGiftVisualBatchKey(msg, targetGroupKey);
|
||||
if (batchKey.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
if (_giftVisualBatchDedupeTimers.containsKey(batchKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final hasStableBatchId = (msg.giftBatchId ?? "").trim().isNotEmpty;
|
||||
final dedupeWindow =
|
||||
hasStableBatchId
|
||||
? _giftVisualBatchDedupeWindow
|
||||
: _giftFlightBatchDedupeWindow;
|
||||
late final Timer timer;
|
||||
timer = Timer(dedupeWindow, () {
|
||||
if (identical(_giftVisualBatchDedupeTimers[batchKey], timer)) {
|
||||
_giftVisualBatchDedupeTimers.remove(batchKey);
|
||||
}
|
||||
});
|
||||
_giftVisualBatchDedupeTimers[batchKey] = timer;
|
||||
return true;
|
||||
}
|
||||
|
||||
String _buildGiftVisualBatchKey(Msg msg, String targetGroupKey) {
|
||||
final stableBatchId = (msg.giftBatchId ?? "").trim();
|
||||
if (stableBatchId.isNotEmpty) {
|
||||
return "batch|$stableBatchId";
|
||||
}
|
||||
|
||||
final timeBucket =
|
||||
((msg.time ?? DateTime.now().millisecondsSinceEpoch) / 150).floor();
|
||||
return [
|
||||
"legacy",
|
||||
msg.type ?? "",
|
||||
msg.groupId ?? "",
|
||||
msg.gift?.id ?? "",
|
||||
msg.user?.id ?? "",
|
||||
targetGroupKey,
|
||||
msg.number?.toString() ?? "",
|
||||
msg.customAnimationCount?.toString() ?? "",
|
||||
timeBucket.toString(),
|
||||
].join("|");
|
||||
}
|
||||
|
||||
String _buildGiftFlightBatchKey(Msg msg, List<String> targetUserIds) {
|
||||
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||
normalizedTargetUserIds.sort();
|
||||
@ -906,6 +1005,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_giftFlightBatchDedupeTimers.clear();
|
||||
}
|
||||
|
||||
void _clearGiftVisualBatchDedupeTimers() {
|
||||
for (final timer in _giftVisualBatchDedupeTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_giftVisualBatchDedupeTimers.clear();
|
||||
}
|
||||
|
||||
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
|
||||
final gift = msg.gift;
|
||||
if (gift == null) {
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_preview_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_upload_page.dart';
|
||||
@ -11,6 +12,13 @@ import 'package:yumi/modules/room/music/room_music_page.dart';
|
||||
import 'package:yumi/modules/room/them/room_theme_page.dart';
|
||||
import 'package:yumi/modules/room/voice_room_page.dart';
|
||||
import 'package:yumi/app/routes/sc_router_init.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
|
||||
|
||||
class VoiceRoomRoute implements SCIRouterProvider {
|
||||
static String voiceRoom = '/room';
|
||||
@ -51,6 +59,49 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> _pushBackgroundRoute<T>(
|
||||
BuildContext context,
|
||||
Route<T> route, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
final restoreEffects = _pauseRoomVisualEffects(context);
|
||||
return Navigator.of(
|
||||
context,
|
||||
rootNavigator: rootNavigator,
|
||||
).push<T>(route).whenComplete(restoreEffects);
|
||||
}
|
||||
|
||||
static VoidCallback _pauseRoomVisualEffects(BuildContext context) {
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
final rtmProvider = context.read<RtmProvider>();
|
||||
final giftAnimationManager = context.read<GiftAnimationManager>();
|
||||
final wasEnabled = rtcProvider.roomVisualEffectsEnabled;
|
||||
final floatingGiftListener = rtmProvider.msgFloatingGiftListener;
|
||||
final luckyGiftRewardTickerListener =
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener;
|
||||
|
||||
rtcProvider.setRoomVisualEffectsEnabled(false);
|
||||
rtmProvider.msgFloatingGiftListener = null;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener = null;
|
||||
RoomEntranceHelper.clearQueue();
|
||||
giftAnimationManager.clearActiveAnimations();
|
||||
OverlayManager().removeRoom();
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route');
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
|
||||
return () {
|
||||
if (!context.mounted || rtcProvider.currenRoom == null) {
|
||||
return;
|
||||
}
|
||||
rtcProvider.setRoomVisualEffectsEnabled(wasEnabled);
|
||||
if (wasEnabled) {
|
||||
rtmProvider.msgFloatingGiftListener ??= floatingGiftListener;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener ??=
|
||||
luckyGiftRewardTickerListener;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Future<T?> openVoiceRoom<T>(
|
||||
BuildContext context, {
|
||||
bool replace = false,
|
||||
@ -58,7 +109,6 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
}) {
|
||||
final navigator = Navigator.of(context, rootNavigator: rootNavigator);
|
||||
if (!replace && isVoiceRoomOpen) {
|
||||
debugPrint('[RoomRoute] skip duplicate voice room route');
|
||||
return Future<T?>.value();
|
||||
}
|
||||
final route = _buildRoute<T>(
|
||||
@ -79,6 +129,29 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
});
|
||||
}
|
||||
|
||||
static void popVoiceRoomToPrevious(
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
popVoiceRoomToPreviousOn(
|
||||
Navigator.of(context, rootNavigator: rootNavigator),
|
||||
);
|
||||
}
|
||||
|
||||
static void popVoiceRoomToPreviousOn(NavigatorState navigator) {
|
||||
if (!isVoiceRoomOpen) {
|
||||
return;
|
||||
}
|
||||
var removedVoiceRoom = false;
|
||||
navigator.popUntil((route) {
|
||||
if (route.settings.name == voiceRoom) {
|
||||
removedVoiceRoom = true;
|
||||
return false;
|
||||
}
|
||||
return removedVoiceRoom || route.isFirst;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<T?> openRoomEdit<T>(
|
||||
BuildContext context, {
|
||||
required bool needRestCurrentRoomInfo,
|
||||
@ -107,11 +180,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
const RoomBackgroundSelectPage(),
|
||||
settings: RouteSettings(name: roomBackgroundSelect),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -119,11 +194,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
const RoomBackgroundUploadPage(),
|
||||
settings: RouteSettings(name: roomBackgroundUpload),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -132,11 +209,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
String? backgroundPath,
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
RoomBackgroundPreviewPage(backgroundPath: backgroundPath),
|
||||
settings: RouteSettings(name: roomBackgroundPreview),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -412,9 +412,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return '';
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
|
||||
@ -385,9 +385,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) {
|
||||
final trimmed = value.trim();
|
||||
@ -523,7 +521,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
<h3>Leader Mock</h3>
|
||||
<p>Use these buttons to verify bridge wiring.</p>
|
||||
<button onclick="loadComplete()">loadComplete()</button>
|
||||
<button onclick="console.log(getConfig && getConfig())">getConfig()</button>
|
||||
<button onclick="getConfig && getConfig()">getConfig()</button>
|
||||
<button onclick="pay()">pay()</button>
|
||||
<button onclick="closeGame()">closeGame()</button>
|
||||
<button onclick="window.updateCoin && window.updateCoin()">updateCoin()</button>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/modules/room_game/data/room_game_repository.dart';
|
||||
import 'package:yumi/modules/room_game/utils/room_game_viewport.dart';
|
||||
@ -14,9 +17,14 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RoomGameListSheet extends StatefulWidget {
|
||||
const RoomGameListSheet({super.key, required this.roomContext});
|
||||
const RoomGameListSheet({
|
||||
super.key,
|
||||
required this.roomContext,
|
||||
this.initialGameId,
|
||||
});
|
||||
|
||||
final BuildContext roomContext;
|
||||
final String? initialGameId;
|
||||
|
||||
@override
|
||||
State<RoomGameListSheet> createState() => _RoomGameListSheetState();
|
||||
@ -35,6 +43,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
|
||||
String _roomId = '';
|
||||
String? _launchingGameId;
|
||||
bool _hasHandledInitialGame = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -53,9 +62,47 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
roomId: _roomId,
|
||||
category: 'CHAT_ROOM',
|
||||
);
|
||||
_scheduleInitialGameOpen(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
void _scheduleInitialGameOpen(List<RoomGameListItemModel> items) {
|
||||
final targetGameId = widget.initialGameId?.trim() ?? '';
|
||||
if (_hasHandledInitialGame || targetGameId.isEmpty || items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_hasHandledInitialGame = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final game = _resolveInitialGame(items, targetGameId);
|
||||
if (game == null) {
|
||||
SCTts.show('Game not found');
|
||||
return;
|
||||
}
|
||||
unawaited(_openGame(game));
|
||||
});
|
||||
}
|
||||
|
||||
RoomGameListItemModel? _resolveInitialGame(
|
||||
List<RoomGameListItemModel> items,
|
||||
String targetGameId,
|
||||
) {
|
||||
final supportedItems =
|
||||
items.where((item) => item.isBaishun || item.isLeader).toList();
|
||||
final candidates = supportedItems.isNotEmpty ? supportedItems : items;
|
||||
if (targetGameId == '0') {
|
||||
return candidates[math.Random().nextInt(candidates.length)];
|
||||
}
|
||||
for (final item in candidates) {
|
||||
if (item.gameId == targetGameId || item.providerGameId == targetGameId) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _retry() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -128,9 +175,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
|
||||
class LastWeeklyCPSplashEntry {
|
||||
@ -83,7 +82,6 @@ class LastWeeklyCPSplashCache {
|
||||
.toList()
|
||||
..sort((a, b) => a.rank.compareTo(b.rank));
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('LastWeeklyCPSplashCache parse failed: $error\n$stackTrace');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
@ -98,8 +96,6 @@ class LastWeeklyCPSplashCache {
|
||||
|
||||
try {
|
||||
// 当前占位逻辑已下线,等待正式 CP 榜接口接入。
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('LastWeeklyCPSplashCache refresh failed: $error\n$stackTrace');
|
||||
}
|
||||
} catch (error, stackTrace) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -495,7 +495,6 @@ class _SplashPageState extends State<SplashPage> {
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
return user;
|
||||
} catch (e) {
|
||||
debugPrint('restore login session failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
@ -72,7 +71,6 @@ class WeeklyStarSplashCache {
|
||||
.toList()
|
||||
..sort((a, b) => a.rank.compareTo(b.rank));
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('WeeklyStarSplashCache parse failed: $error\n$stackTrace');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
@ -95,9 +93,7 @@ class WeeklyStarSplashCache {
|
||||
_cacheKey,
|
||||
jsonEncode(entries.map((entry) => entry.toJson()).toList()),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('WeeklyStarSplashCache refresh failed: $error\n$stackTrace');
|
||||
}
|
||||
} catch (error, stackTrace) {}
|
||||
}
|
||||
|
||||
static List<WeeklyStarSplashEntry> _mapTopThree(
|
||||
|
||||
@ -146,9 +146,6 @@ class _CropImageRouteState extends State<CropImagePage> {
|
||||
}) async {
|
||||
final originalExists = originalFile.existsSync();
|
||||
final originalSize = originalExists ? originalFile.lengthSync() : -1;
|
||||
debugPrint(
|
||||
"[Room Cover Upload] crop start originalPath=${originalFile.path} originalExists=$originalExists originalSize=$originalSize needUpload=$needUpload aspectRatio=${widget.aspectRatio}",
|
||||
);
|
||||
// 调用系统裁剪界面
|
||||
CroppedFile? cropped = await ImageCropper().cropImage(
|
||||
sourcePath: originalFile.path,
|
||||
@ -180,11 +177,7 @@ class _CropImageRouteState extends State<CropImagePage> {
|
||||
if (cropped == null) {
|
||||
if (widget.backOriginalFile ?? true) {
|
||||
fileToUpload = originalFile;
|
||||
debugPrint(
|
||||
"[Room Cover Upload] crop cancelled, fallback to original file path=${fileToUpload.path}",
|
||||
);
|
||||
} else {
|
||||
debugPrint("[Room Cover Upload] crop cancelled, no fallback file");
|
||||
widget.onUpLoadCallBack?.call(false, "");
|
||||
return;
|
||||
}
|
||||
@ -192,9 +185,6 @@ class _CropImageRouteState extends State<CropImagePage> {
|
||||
fileToUpload = File(cropped.path);
|
||||
final croppedExists = fileToUpload.existsSync();
|
||||
final croppedSize = croppedExists ? fileToUpload.lengthSync() : -1;
|
||||
debugPrint(
|
||||
"[Room Cover Upload] crop result path=${fileToUpload.path} exists=$croppedExists size=$croppedSize",
|
||||
);
|
||||
}
|
||||
if (needUpload) {
|
||||
// 弹出上传中的 loading
|
||||
@ -211,20 +201,12 @@ class _CropImageRouteState extends State<CropImagePage> {
|
||||
try {
|
||||
final fileExists = file.existsSync();
|
||||
final fileSize = fileExists ? file.lengthSync() : -1;
|
||||
debugPrint(
|
||||
"[Room Cover Upload] local file before request path=${file.path} exists=$fileExists size=$fileSize",
|
||||
);
|
||||
String fileUrl = await SCGeneralRepositoryImp().upload(file);
|
||||
debugPrint("[Room Cover Upload] upload success fileUrl=$fileUrl");
|
||||
SCLoadingManager.hide();
|
||||
SCNavigatorUtils.goBack(context);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
widget.onUpLoadCallBack?.call(true, fileUrl);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint(
|
||||
"[Room Cover Upload] upload failed path=${file.path} error=$e",
|
||||
);
|
||||
debugPrint("[Room Cover Upload] stackTrace=$stackTrace");
|
||||
SCTts.show("upload fail $e");
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
|
||||
@ -223,7 +223,6 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
||||
String url,
|
||||
) {
|
||||
if (success) {
|
||||
debugPrint("[Profile Avatar] uploaded url: $url");
|
||||
userCover = url;
|
||||
setState(() {});
|
||||
submitAvatarOnly();
|
||||
@ -432,9 +431,6 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
||||
}
|
||||
_isSubmitting = true;
|
||||
SCLoadingManager.show();
|
||||
debugPrint(
|
||||
"[Profile Edit] incremental payload: {userAvatar: ${identical(userAvatarValue, _noChange) ? "<skip>" : userAvatarValue}, userNickname: ${identical(userNicknameValue, _noChange) ? "<skip>" : userNicknameValue}, userSex: ${identical(userSexValue, _noChange) ? "<skip>" : userSexValue}, age: ${identical(ageValue, _noChange) ? "<skip>" : ageValue}, bornYear: ${identical(bornYearValue, _noChange) ? "<skip>" : bornYearValue}, bornMonth: ${identical(bornMonthValue, _noChange) ? "<skip>" : bornMonthValue}, bornDay: ${identical(bornDayValue, _noChange) ? "<skip>" : bornDayValue}, hobby: ${identical(hobbyValue, _noChange) ? "<skip>" : hobbyValue}, autograph: ${identical(autographValue, _noChange) ? "<skip>" : autographValue}}",
|
||||
);
|
||||
try {
|
||||
final updatedProfile = await SCAccountRepository().updateUserInfo(
|
||||
userAvatar:
|
||||
@ -507,9 +503,6 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
||||
? birthdayDate?.year
|
||||
: bornYearValue as num?),
|
||||
);
|
||||
debugPrint(
|
||||
"[Profile Edit] merged profile avatar: ${mergedProfile.userAvatar ?? ""}",
|
||||
);
|
||||
_syncLocalProfileState(mergedProfile);
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -523,7 +516,6 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
||||
setState(() {});
|
||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
SCLoadingManager.hide();
|
||||
|
||||
@ -44,7 +44,10 @@ class _FansUserListPageState
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: SocialChatStandardAppBar(title: SCAppLocalizations.of(context)!.fansList, actions: []),
|
||||
appBar: SocialChatStandardAppBar(
|
||||
title: SCAppLocalizations.of(context)!.fansList,
|
||||
actions: [],
|
||||
),
|
||||
body: buildList(context),
|
||||
),
|
||||
],
|
||||
@ -63,6 +66,7 @@ class _FansUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
@ -78,17 +82,17 @@ class _FansUserListPageState
|
||||
textColor: Colors.white,
|
||||
type: res.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
res.userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
|
||||
@ -66,6 +66,7 @@ class _FollowUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
|
||||
@ -79,12 +79,6 @@ class _MePage2State extends State<MePage2> {
|
||||
|
||||
try {
|
||||
final status = await _loadVipEntryStatus();
|
||||
debugPrint(
|
||||
'[VIP][MeEntry] status loaded active=${status.active} '
|
||||
'level=${status.level} levelCode=${status.levelCode} '
|
||||
'expireAt=${status.expireAt} '
|
||||
'badge=${_vipEntryBadgeLogValue(status.badge)}',
|
||||
);
|
||||
if (!mounted) return;
|
||||
_syncCurrentProfileVipStatus(status);
|
||||
setState(() {
|
||||
@ -92,7 +86,6 @@ class _MePage2State extends State<MePage2> {
|
||||
_isVipStatusLoading = false;
|
||||
});
|
||||
} catch (error) {
|
||||
debugPrint('[VIP][MeEntry] status load failed error=$error');
|
||||
if (!mounted) return;
|
||||
setState(() => _isVipStatusLoading = false);
|
||||
}
|
||||
@ -104,13 +97,9 @@ class _MePage2State extends State<MePage2> {
|
||||
final home = await repository.vipHome();
|
||||
final state = home.state;
|
||||
if (state != null && state.levelInt > 0) {
|
||||
debugPrint('[VIP][MeEntry] use vip home state');
|
||||
return state;
|
||||
}
|
||||
debugPrint('[VIP][MeEntry] vip home state empty, fallback status');
|
||||
} catch (error) {
|
||||
debugPrint('[VIP][MeEntry] vip home load failed error=$error');
|
||||
}
|
||||
} catch (error) {}
|
||||
return repository.vipStatus();
|
||||
}
|
||||
|
||||
@ -134,7 +123,6 @@ class _MePage2State extends State<MePage2> {
|
||||
}
|
||||
|
||||
void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) {
|
||||
debugPrint('[VIP][MeEntry] refresh user data after vip page return');
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
profileManager.balance();
|
||||
_loadCounter();
|
||||
@ -239,6 +227,7 @@ class _MePage2State extends State<MePage2> {
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
headdress: profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: profile?.getHeaddress()?.cover,
|
||||
showDefault: true,
|
||||
),
|
||||
],
|
||||
@ -320,6 +309,12 @@ class _MePage2State extends State<MePage2> {
|
||||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.level,
|
||||
iconPath: 'sc_images/index/sc_icon_level.png',
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.bag,
|
||||
iconPath: 'sc_images/index/sc_icon_bag.png',
|
||||
@ -820,6 +815,7 @@ class _MePage2State extends State<MePage2> {
|
||||
required VoidCallback onTap,
|
||||
double? iconWidth,
|
||||
double? iconHeight,
|
||||
int badgeCount = 0,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: SCDebounceWidget(
|
||||
@ -832,26 +828,33 @@ class _MePage2State extends State<MePage2> {
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
iconPath,
|
||||
width: (iconWidth ?? 54).w,
|
||||
height: (iconHeight ?? 54).w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
iconPath,
|
||||
width: (iconWidth ?? 54).w,
|
||||
height: (iconHeight ?? 54).w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (badgeCount > 0) _buildEntryBadge(badgeCount),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -859,6 +862,35 @@ class _MePage2State extends State<MePage2> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntryBadge(int count) {
|
||||
final text = count > 99 ? '99+' : count.toString();
|
||||
return PositionedDirectional(
|
||||
top: 11.w,
|
||||
end: 14.w,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minWidth: 18.w),
|
||||
height: 18.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.w),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF4B4B),
|
||||
borderRadius: BorderRadius.circular(9.w),
|
||||
border: Border.all(color: Colors.white, width: 1.w),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem({
|
||||
required String value,
|
||||
required String label,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
654
lib/modules/user/profile/profile_gift_wall_detail_page.dart
Normal file
654
lib/modules/user/profile/profile_gift_wall_detail_page.dart
Normal file
@ -0,0 +1,654 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class ProfileGiftWallDetailPage extends StatefulWidget {
|
||||
const ProfileGiftWallDetailPage({super.key, required this.tageId});
|
||||
|
||||
final String tageId;
|
||||
|
||||
@override
|
||||
State<ProfileGiftWallDetailPage> createState() =>
|
||||
_ProfileGiftWallDetailPageState();
|
||||
}
|
||||
|
||||
class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
|
||||
static const Color _profileBg = Color(0xff072121);
|
||||
static const Color _cardBg = Color(0xff08251E);
|
||||
static const Color _profileBorder = Color(0xffB2FBCC);
|
||||
|
||||
SocialChatUserProfile? _profile;
|
||||
List<SocialChatGiftRes> _gifts = [];
|
||||
bool _loadingGifts = true;
|
||||
String? _playingGiftSvgaUrl;
|
||||
int _giftEffectSerial = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_primeProfile();
|
||||
_loadProfile();
|
||||
_loadGiftWall();
|
||||
}
|
||||
|
||||
void _primeProfile() {
|
||||
final currentProfile =
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).userProfile;
|
||||
if (currentProfile?.id == widget.tageId) {
|
||||
_profile = currentProfile;
|
||||
}
|
||||
}
|
||||
|
||||
void _loadProfile() {
|
||||
SCAccountRepository()
|
||||
.loadUserInfo(widget.tageId)
|
||||
.then((profile) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_profile = profile;
|
||||
});
|
||||
})
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
Future<void> _loadGiftWall() async {
|
||||
final appGeneralManager = Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
final result = await SCGiftRepositoryImp().giftWall(widget.tageId);
|
||||
final resolvedGifts = await _resolveGiftWallMeta(
|
||||
result,
|
||||
appGeneralManager,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_gifts = resolvedGifts;
|
||||
_loadingGifts = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loadingGifts = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SocialChatGiftRes>> _resolveGiftWallMeta(
|
||||
List<SocialChatGiftRes> gifts,
|
||||
SCAppGeneralManager appGeneralManager,
|
||||
) async {
|
||||
if (gifts.isEmpty) return gifts;
|
||||
if (appGeneralManager.giftResList.isEmpty) {
|
||||
await appGeneralManager.giftList(includeCustomized: false);
|
||||
}
|
||||
return gifts
|
||||
.map((gift) => _mergeGiftMeta(gift, appGeneralManager))
|
||||
.toList();
|
||||
}
|
||||
|
||||
SocialChatGiftRes _mergeGiftMeta(
|
||||
SocialChatGiftRes gift,
|
||||
SCAppGeneralManager appGeneralManager,
|
||||
) {
|
||||
final meta = _findGiftMeta(gift, appGeneralManager);
|
||||
if (meta == null) return gift;
|
||||
return gift.copyWith(
|
||||
giftName: _firstNonBlank([gift.giftName, meta.giftName]),
|
||||
giftCandy: gift.giftCandy ?? meta.giftCandy,
|
||||
giftIntegral: gift.giftIntegral ?? meta.giftIntegral,
|
||||
giftPhoto: _firstNonBlank([gift.giftPhoto, meta.giftPhoto]),
|
||||
giftSourceUrl: _firstNonBlank([gift.giftSourceUrl, meta.giftSourceUrl]),
|
||||
giftCode: _firstNonBlank([gift.giftCode, meta.giftCode]),
|
||||
standardId: _firstNonBlank([gift.standardId, meta.standardId]),
|
||||
);
|
||||
}
|
||||
|
||||
SocialChatGiftRes? _findGiftMeta(
|
||||
SocialChatGiftRes gift,
|
||||
SCAppGeneralManager appGeneralManager,
|
||||
) {
|
||||
final idCandidates = <String>[
|
||||
gift.standardId ?? "",
|
||||
gift.id ?? "",
|
||||
gift.giftCode ?? "",
|
||||
].where((id) => id.trim().isNotEmpty && id.trim() != "0");
|
||||
|
||||
for (final id in idCandidates) {
|
||||
final meta = appGeneralManager.getGiftByIdOrStandardId(id);
|
||||
if (meta != null) return meta;
|
||||
}
|
||||
|
||||
final giftPhoto = (gift.giftPhoto ?? "").trim();
|
||||
final giftCode = (gift.giftCode ?? "").trim();
|
||||
for (final meta in appGeneralManager.giftResList) {
|
||||
if (giftPhoto.isNotEmpty && giftPhoto == (meta.giftPhoto ?? "").trim()) {
|
||||
return meta;
|
||||
}
|
||||
if (giftCode.isNotEmpty && giftCode == (meta.giftCode ?? "").trim()) {
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int get _receivedCount {
|
||||
return _gifts.fold<int>(0, (sum, gift) => sum + _giftQuantity(gift));
|
||||
}
|
||||
|
||||
num get _totalValue {
|
||||
return _gifts.fold<num>(
|
||||
0,
|
||||
(sum, gift) => sum + (_giftAmount(gift) * _giftQuantity(gift)),
|
||||
);
|
||||
}
|
||||
|
||||
int _giftQuantity(SocialChatGiftRes gift) {
|
||||
return int.tryParse(gift.quantity ?? "") ?? 0;
|
||||
}
|
||||
|
||||
num _giftAmount(SocialChatGiftRes gift) {
|
||||
return gift.giftCandy ?? gift.giftIntegral ?? 0;
|
||||
}
|
||||
|
||||
String _formatNumber(num value) {
|
||||
if (value % 1 == 0) return value.toInt().toString();
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
bool _isSvgaGift(SocialChatGiftRes gift) {
|
||||
final sourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
return sourceUrl.isNotEmpty &&
|
||||
SCPathUtils.getFileExtension(sourceUrl).toLowerCase() == ".svga";
|
||||
}
|
||||
|
||||
void _playGiftSvgaOnce(SocialChatGiftRes gift) {
|
||||
final sourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
if (!_isSvgaGift(gift) || _playingGiftSvgaUrl != null) {
|
||||
return;
|
||||
}
|
||||
SCGiftVapSvgaManager().preload(sourceUrl, highPriority: true);
|
||||
setState(() {
|
||||
_playingGiftSvgaUrl = sourceUrl;
|
||||
_giftEffectSerial++;
|
||||
});
|
||||
}
|
||||
|
||||
void _clearGiftSvgaEffect() {
|
||||
if (!mounted || _playingGiftSvgaUrl == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_playingGiftSvgaUrl = null;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildGradientBorder({
|
||||
double? width,
|
||||
double? height,
|
||||
required double radius,
|
||||
required Widget child,
|
||||
Color backgroundColor = _cardBg,
|
||||
double endAlpha = 0.42,
|
||||
}) {
|
||||
final borderRadius = BorderRadius.circular(radius);
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [_profileBorder, _profileBorder.withValues(alpha: endAlpha)],
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(1.w),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular((radius - 1.w).clamp(0, radius)),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 54.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 8.w,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_left_rounded,
|
||||
color: Colors.white,
|
||||
size: 38.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Gift Wall",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileHeader() {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: 12.w),
|
||||
head(
|
||||
url: _profile?.userAvatar ?? "",
|
||||
width: 104.w,
|
||||
headdress: _profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: _profile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
socialchatNickNameText(
|
||||
_profile?.userNickname ?? "",
|
||||
maxWidth: 300.w,
|
||||
fontSize: 20,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
type: _profile?.getVIP()?.name ?? "",
|
||||
needScroll: (_profile?.userNickname?.characters.length ?? 0) > 13,
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
_buildSummaryCard(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard() {
|
||||
return _buildGradientBorder(
|
||||
width: 355.w,
|
||||
height: 51.w,
|
||||
radius: 8.w,
|
||||
backgroundColor: _cardBg,
|
||||
endAlpha: 0.52,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildSummaryItem(
|
||||
value: _formatNumber(_receivedCount),
|
||||
label: "Received",
|
||||
),
|
||||
_buildSummaryDivider(),
|
||||
_buildSummaryItem(
|
||||
value: _formatNumber(_totalValue),
|
||||
label: "Total Value",
|
||||
showCoin: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryItem({
|
||||
required String value,
|
||||
required String label,
|
||||
bool showCoin = false,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showCoin) ...[_buildCoinIcon(14.w), SizedBox(width: 3.w)],
|
||||
text(
|
||||
value,
|
||||
fontSize: 17,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
text(label, fontSize: 14, textColor: const Color(0xffB1B1B1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryDivider() {
|
||||
return Container(
|
||||
height: 28.w,
|
||||
width: 1.w,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromRGBO(255, 255, 255, 0),
|
||||
Colors.white,
|
||||
Color.fromRGBO(255, 255, 255, 0),
|
||||
],
|
||||
stops: [0, 0.4862, 1],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiftGrid() {
|
||||
if (_loadingGifts) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 80.w),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: _profileBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_gifts.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 90.w),
|
||||
child: Opacity(
|
||||
opacity: 0.42,
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_icon_room_music_empty.png",
|
||||
width: 72.w,
|
||||
height: 72.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(10.w, 34.w, 10.w, 0),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildGiftItem(_gifts[index]),
|
||||
childCount: _gifts.length,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 25.w,
|
||||
crossAxisSpacing: 22.w,
|
||||
childAspectRatio: 104 / 130,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiftItem(SocialChatGiftRes gift) {
|
||||
final quantity = _giftQuantity(gift);
|
||||
final giftName =
|
||||
(gift.giftName ?? "").trim().isEmpty ? "--" : gift.giftName!.trim();
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _playGiftSvgaOnce(gift),
|
||||
child: _buildGradientBorder(
|
||||
radius: 8.w,
|
||||
backgroundColor: _cardBg,
|
||||
endAlpha: 0.62,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 8.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: netImage(
|
||||
url: gift.giftPhoto ?? "",
|
||||
width: 82.w,
|
||||
height: 74.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
text(
|
||||
"$giftName*$quantity",
|
||||
fontSize: 13,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildCoinIcon(12.w),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: text(
|
||||
_formatNumber(_giftAmount(gift)),
|
||||
fontSize: 11,
|
||||
textColor: Colors.white,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoinIcon(double size) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Image.asset("sc_images/room/sc_icon_luckgift_coins_anim.webp"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom + 22.w;
|
||||
final playingGiftSvgaUrl = _playingGiftSvgaUrl;
|
||||
return Scaffold(
|
||||
backgroundColor: _profileBg,
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [_buildTopBar(), _buildProfileHeader()],
|
||||
),
|
||||
),
|
||||
_buildGiftGrid(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 54.w, bottom: bottomPadding),
|
||||
child: Center(
|
||||
child: text(
|
||||
"No Further Data Available~",
|
||||
fontSize: 13,
|
||||
textColor: const Color(0xffB1B1B1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (playingGiftSvgaUrl != null)
|
||||
Positioned.fill(
|
||||
child: _GiftWallSvgaEffectOverlay(
|
||||
key: ValueKey("$playingGiftSvgaUrl-$_giftEffectSerial"),
|
||||
sourceUrl: playingGiftSvgaUrl,
|
||||
onFinished: _clearGiftSvgaEffect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GiftWallSvgaEffectOverlay extends StatefulWidget {
|
||||
const _GiftWallSvgaEffectOverlay({
|
||||
super.key,
|
||||
required this.sourceUrl,
|
||||
required this.onFinished,
|
||||
});
|
||||
|
||||
final String sourceUrl;
|
||||
final VoidCallback onFinished;
|
||||
|
||||
@override
|
||||
State<_GiftWallSvgaEffectOverlay> createState() =>
|
||||
_GiftWallSvgaEffectOverlayState();
|
||||
}
|
||||
|
||||
class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SVGAAnimationController _controller;
|
||||
bool _loaded = false;
|
||||
bool _finished = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SVGAAnimationController(vsync: this);
|
||||
_controller.addStatusListener(_handleStatusChanged);
|
||||
_loadAndPlay();
|
||||
}
|
||||
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_finish();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAndPlay() async {
|
||||
try {
|
||||
final movieEntity = await _loadMovieEntity(widget.sourceUrl);
|
||||
if (!mounted || _finished) return;
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
_controller
|
||||
..reset()
|
||||
..repeat(count: 1);
|
||||
} catch (error) {
|
||||
_finish();
|
||||
}
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadMovieEntity(String sourceUrl) async {
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
final cached = cache[sourceUrl];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final pathType = SCPathUtils.getPathType(sourceUrl);
|
||||
late final MovieEntity movieEntity;
|
||||
if (pathType == PathType.network) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl);
|
||||
} else if (pathType == PathType.asset) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl);
|
||||
} else if (pathType == PathType.file) {
|
||||
final bytes = await File(sourceUrl).readAsBytes();
|
||||
movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes);
|
||||
} else {
|
||||
throw Exception("Unsupported SVGA source: $sourceUrl");
|
||||
}
|
||||
movieEntity.autorelease = false;
|
||||
cache[sourceUrl] = movieEntity;
|
||||
return movieEntity;
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
widget.onFinished();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_handleStatusChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
alignment: Alignment.center,
|
||||
child:
|
||||
_loaded
|
||||
? SVGAImage(
|
||||
_controller,
|
||||
fit: BoxFit.contain,
|
||||
allowDrawingOverflow: false,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/modules/user/profile/profile_wall_of_honors_page.dart
Normal file
152
lib/modules/user/profile/profile_wall_of_honors_page.dart
Normal file
@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class ProfileWallOfHonorsPage extends StatefulWidget {
|
||||
const ProfileWallOfHonorsPage({super.key, required this.tageId});
|
||||
|
||||
final String tageId;
|
||||
|
||||
@override
|
||||
State<ProfileWallOfHonorsPage> createState() =>
|
||||
_ProfileWallOfHonorsPageState();
|
||||
}
|
||||
|
||||
class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
|
||||
static const Color _profileBg = Color(0xff072121);
|
||||
static const Color _mutedText = Color(0xff8F9A98);
|
||||
|
||||
int _selectedTabIndex = 0;
|
||||
final int _honorCount = 0;
|
||||
final int _eventCount = 0;
|
||||
|
||||
int get _totalCount => _honorCount + _eventCount;
|
||||
|
||||
Widget _buildTopBar() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 54.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 8.w,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_left_rounded,
|
||||
color: Colors.white,
|
||||
size: 38.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"My Achievements($_totalCount)",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabs() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 82.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildTab(index: 0, label: "Honors($_honorCount)")),
|
||||
Expanded(child: _buildTab(index: 1, label: "Event($_eventCount)")),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab({required int index, required String label}) {
|
||||
final selected = _selectedTabIndex == index;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_selectedTabIndex == index) return;
|
||||
setState(() {
|
||||
_selectedTabIndex = index;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
text(
|
||||
label,
|
||||
fontSize: 20,
|
||||
textColor: selected ? Colors.white : _mutedText,
|
||||
fontWeight: FontWeight.w400,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 9.w),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: selected ? 28.w : 0,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
text(
|
||||
"Stay Tuned For More Achievements!",
|
||||
fontSize: 13,
|
||||
textColor: const Color(0xffA4AAA8),
|
||||
fontWeight: FontWeight.w400,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 120.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _profileBg,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(children: [_buildTopBar(), _buildTabs()]),
|
||||
),
|
||||
_buildEmptyState(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
247
lib/modules/user/settings/broadcast/broadcast_settings_page.dart
Normal file
247
lib/modules/user/settings/broadcast/broadcast_settings_page.dart
Normal file
@ -0,0 +1,247 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class BroadcastSettingsPage extends StatefulWidget {
|
||||
const BroadcastSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<BroadcastSettingsPage> createState() => _BroadcastSettingsPageState();
|
||||
}
|
||||
|
||||
class _BroadcastSettingsPageState extends State<BroadcastSettingsPage> {
|
||||
static const String _scopePickerTag = "showBroadcastScopePicker";
|
||||
|
||||
late String _selectedScope;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedScope = _readSavedScope();
|
||||
SCGlobalConfig.floatingBroadcastScope = _selectedScope;
|
||||
}
|
||||
|
||||
String? get _account =>
|
||||
AccountStorage().getCurrentUser()?.userProfile?.account;
|
||||
|
||||
String _readSavedScope() {
|
||||
final savedScope = DataPersistence.getString(
|
||||
SCGlobalConfig.floatingBroadcastScopeStorageKey(_account),
|
||||
);
|
||||
if (savedScope.isNotEmpty) {
|
||||
return SCGlobalConfig.normalizeFloatingBroadcastScope(savedScope);
|
||||
}
|
||||
final isLegacyEnabled = DataPersistence.getBool(
|
||||
SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(_account),
|
||||
defaultValue: true,
|
||||
);
|
||||
return isLegacyEnabled
|
||||
? SCGlobalConfig.floatingBroadcastScopeAll
|
||||
: SCGlobalConfig.floatingBroadcastScopeOff;
|
||||
}
|
||||
|
||||
String _scopeTitle(BuildContext context, String scope) {
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
switch (SCGlobalConfig.normalizeFloatingBroadcastScope(scope)) {
|
||||
case SCGlobalConfig.floatingBroadcastScopeOff:
|
||||
return l10n.broadcastOff;
|
||||
case SCGlobalConfig.floatingBroadcastScopeRoom:
|
||||
return l10n.broadcastRoomOnly;
|
||||
case SCGlobalConfig.floatingBroadcastScopeAll:
|
||||
default:
|
||||
return l10n.broadcastAllPlaces;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectScope(String scope) async {
|
||||
final normalizedScope = SCGlobalConfig.normalizeFloatingBroadcastScope(
|
||||
scope,
|
||||
);
|
||||
setState(() {
|
||||
_selectedScope = normalizedScope;
|
||||
});
|
||||
SCGlobalConfig.floatingBroadcastScope = normalizedScope;
|
||||
OverlayManager().refreshDisplayPreference();
|
||||
await DataPersistence.setString(
|
||||
SCGlobalConfig.floatingBroadcastScopeStorageKey(_account),
|
||||
normalizedScope,
|
||||
);
|
||||
await DataPersistence.setBool(
|
||||
SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(_account),
|
||||
normalizedScope != SCGlobalConfig.floatingBroadcastScopeOff,
|
||||
);
|
||||
SmartDialog.dismiss(tag: _scopePickerTag);
|
||||
}
|
||||
|
||||
void _showScopePicker() {
|
||||
SmartDialog.show(
|
||||
tag: _scopePickerTag,
|
||||
alignment: Alignment.bottomCenter,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(10.w)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildScopeOption(SCGlobalConfig.floatingBroadcastScopeAll),
|
||||
Divider(height: 1.w, color: const Color(0xffeeeeee)),
|
||||
_buildScopeOption(SCGlobalConfig.floatingBroadcastScopeRoom),
|
||||
Divider(height: 1.w, color: const Color(0xffeeeeee)),
|
||||
_buildScopeOption(SCGlobalConfig.floatingBroadcastScopeOff),
|
||||
Container(height: 6.w, color: const Color(0xfff5f5f5)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SmartDialog.dismiss(tag: _scopePickerTag),
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
alignment: Alignment.center,
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.cancel,
|
||||
textColor: Colors.black45,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScopeOption(String scope) {
|
||||
final selected =
|
||||
SCGlobalConfig.normalizeFloatingBroadcastScope(_selectedScope) ==
|
||||
SCGlobalConfig.normalizeFloatingBroadcastScope(scope);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _selectScope(scope),
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: text(
|
||||
_scopeTitle(context, scope),
|
||||
textColor: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 18.w,
|
||||
child:
|
||||
selected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 16.w,
|
||||
color: const Color(0xff18F2B1),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getSettingsPageBackgroundImage(),
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: SocialChatStandardAppBar(
|
||||
title: SCAppLocalizations.of(context)!.broadcast,
|
||||
actions: [],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBorderColor(),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: SCDebounceWidget(
|
||||
onTap: _showScopePicker,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 18.w),
|
||||
Expanded(
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.broadcastDisplay,
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPagePrimaryTextColor(),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
text(
|
||||
_scopeTitle(context, _selectedScope),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageSecondaryTextColor(),
|
||||
fontSize: 12.sp,
|
||||
maxWidth: 120.w,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Icon(
|
||||
SCGlobalConfig.lang == "ar"
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right,
|
||||
size: 15.w,
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageIconColor(),
|
||||
),
|
||||
SizedBox(width: 18.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,21 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/modules/user/settings/settings_route.dart';
|
||||
|
||||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_app_utils.dart';
|
||||
|
||||
///设置页面
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
_SettingsPageState createState() => _SettingsPageState();
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
@ -56,7 +52,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
margin: EdgeInsetsDirectional.only(start: 15.w),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.account,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageSecondaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageSecondaryTextColor(),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
@ -65,8 +63,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageMainContainerBackgroundColor(),
|
||||
border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageMainContainerBorderColor(), width: 1.w),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageMainContainerBackgroundColor(),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageMainContainerBorderColor(),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@ -76,7 +81,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SizedBox(width: 18.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.account,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPagePrimaryTextColor(),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
Spacer(),
|
||||
@ -85,7 +92,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right,
|
||||
size: 15.w,
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageIconColor(),
|
||||
),
|
||||
SizedBox(width: 18.w),
|
||||
],
|
||||
@ -106,8 +115,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
margin: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBorderColor(), width: 1.w),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBorderColor(),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@ -117,7 +133,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SizedBox(width: 18.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.blockedList,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPagePrimaryTextColor(),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
Spacer(),
|
||||
@ -126,7 +144,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right,
|
||||
size: 15.w,
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageIconColor(),
|
||||
),
|
||||
SizedBox(width: 18.w),
|
||||
],
|
||||
@ -147,8 +167,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
margin: EdgeInsetsDirectional.only(start: 15.w),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.about,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageAboutTextColor(),
|
||||
SCAppLocalizations.of(context)!.other,
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageAboutTextColor(),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
@ -157,8 +179,79 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBorderColor(), width: 1.w),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBorderColor(),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
SettingsRoute.broadcast,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 18.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.broadcast,
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPagePrimaryTextColor(),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
Spacer(),
|
||||
Icon(
|
||||
SCGlobalConfig.lang == "ar"
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right,
|
||||
size: 15.w,
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageIconColor(),
|
||||
),
|
||||
SizedBox(width: 18.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Container(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
margin: EdgeInsetsDirectional.only(start: 15.w),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.about,
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageAboutTextColor(),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBackgroundColor(),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageContainerBorderColor(),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@ -175,7 +268,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SizedBox(width: 18.w),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.aboutUs,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPagePrimaryTextColor(),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
Spacer(),
|
||||
@ -184,7 +279,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
? Icons.keyboard_arrow_left
|
||||
: Icons.keyboard_arrow_right,
|
||||
size: 15.w,
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageIconColor(),
|
||||
),
|
||||
SizedBox(width: 18.w),
|
||||
],
|
||||
@ -193,7 +290,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 45.w,),
|
||||
SizedBox(height: 45.w),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -207,12 +304,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getSettingsPageButtonBackgroundColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageButtonBackgroundColor(),
|
||||
),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.logout,
|
||||
fontSize: 14.sp,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageButtonTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getSettingsPageButtonTextColor(),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:fluro/src/fluro_router.dart';
|
||||
import 'package:yumi/modules/user/settings/account/set/pwd/reset_pwd_page.dart';
|
||||
import 'package:yumi/modules/user/settings/account/set/pwd/set_pwd_page.dart';
|
||||
import 'package:yumi/modules/user/settings/settings_page.dart';
|
||||
@ -10,6 +9,7 @@ import 'package:yumi/modules/user/settings/account/account_page.dart';
|
||||
import 'package:yumi/modules/user/settings/account/delete/delete_account_page.dart';
|
||||
import 'package:yumi/modules/user/settings/account/set/set_account_page.dart';
|
||||
import 'package:yumi/modules/user/settings/blacklist/user_blacklist_page.dart';
|
||||
import 'package:yumi/modules/user/settings/broadcast/broadcast_settings_page.dart';
|
||||
|
||||
class SettingsRoute implements SCIRouterProvider {
|
||||
static String settings = '/me/settings';
|
||||
@ -20,12 +20,13 @@ class SettingsRoute implements SCIRouterProvider {
|
||||
static String resetPwd = '/me/settings/account/setAccount/resetPwd';
|
||||
static String setPwd = '/me/settings/account/setAccount/setPwd';
|
||||
static String about = '/me/settings/account/about';
|
||||
static String broadcast = '/me/settings/broadcast';
|
||||
|
||||
@override
|
||||
void initRouter(FluroRouter router) {
|
||||
router.define(
|
||||
settings,
|
||||
handler: Handler(handlerFunc: (_, params) => SettingsPage()),
|
||||
handler: Handler(handlerFunc: (_, params) => const SettingsPage()),
|
||||
);
|
||||
router.define(
|
||||
account,
|
||||
@ -53,10 +54,16 @@ class SettingsRoute implements SCIRouterProvider {
|
||||
handler: Handler(handlerFunc: (_, params) => AboutPage()),
|
||||
);
|
||||
|
||||
router.define(
|
||||
broadcast,
|
||||
handler: Handler(
|
||||
handlerFunc: (_, params) => const BroadcastSettingsPage(),
|
||||
),
|
||||
);
|
||||
|
||||
router.define(
|
||||
blockedList,
|
||||
handler: Handler(handlerFunc: (_, params) => UserBlockedListPage()),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,61 +5,128 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/main.dart' show routeObserver;
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart';
|
||||
|
||||
class TaskPage extends StatefulWidget {
|
||||
const TaskPage({super.key});
|
||||
const TaskPage({
|
||||
super.key,
|
||||
this.showBackButton = true,
|
||||
this.onTaskStatusChanged,
|
||||
});
|
||||
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onTaskStatusChanged;
|
||||
|
||||
@override
|
||||
State<TaskPage> createState() => _TaskPageState();
|
||||
}
|
||||
|
||||
class _TaskPageState extends State<TaskPage> {
|
||||
static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png';
|
||||
static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png';
|
||||
class _TaskPageState extends State<TaskPage>
|
||||
with WidgetsBindingObserver, RouteAware {
|
||||
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
||||
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
|
||||
|
||||
final SCAccountRepository _repository = SCAccountRepository();
|
||||
final Set<num> _claimedTaskIds = {};
|
||||
final Set<num> _claimingTaskIds = {};
|
||||
final Set<String> _claimedTaskIds = {};
|
||||
final Set<String> _claimingTaskIds = {};
|
||||
List<SCTaskListRes> _tasks = [];
|
||||
bool _loading = true;
|
||||
bool _loadingTasks = false;
|
||||
String? _toastMessage;
|
||||
Timer? _toastTimer;
|
||||
Timer? _cacheRefreshTimer;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_loadTasks();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
routeObserver.unsubscribe(this);
|
||||
_toastTimer?.cancel();
|
||||
_cacheRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
|
||||
if (_routeObserverRoute != null) {
|
||||
routeObserver.unsubscribe(this);
|
||||
}
|
||||
_routeObserverRoute = route;
|
||||
routeObserver.subscribe(this, route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('app resumed');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('route return');
|
||||
}
|
||||
|
||||
Future<void> _loadTasks() async {
|
||||
if (_loadingTasks) {
|
||||
return;
|
||||
}
|
||||
_loadingTasks = true;
|
||||
try {
|
||||
final tasks = await _repository.tasks();
|
||||
if (!mounted) return;
|
||||
final sortedTasks =
|
||||
tasks..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
final dailyCount =
|
||||
sortedTasks.where((task) => (task.taskType ?? 0).toInt() == 0).length;
|
||||
final exclusiveCount = sortedTasks.length - dailyCount;
|
||||
setState(() {
|
||||
_tasks =
|
||||
tasks
|
||||
..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
_tasks = sortedTasks;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (_) {
|
||||
widget.onTaskStatusChanged?.call();
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_tasks = [];
|
||||
_loading = false;
|
||||
});
|
||||
} finally {
|
||||
_loadingTasks = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleManualRefresh() async {
|
||||
await _loadTasks();
|
||||
_scheduleTaskStatusCacheRefresh('manual refresh');
|
||||
}
|
||||
|
||||
void _scheduleTaskStatusCacheRefresh(String reason) {
|
||||
_cacheRefreshTimer?.cancel();
|
||||
_cacheRefreshTimer = Timer(_taskStatusCacheRefreshDelay, () {
|
||||
_cacheRefreshTimer = null;
|
||||
if (!mounted) return;
|
||||
unawaited(_loadTasks());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dailyTasks = _sectionTasks(isNewcomer: false);
|
||||
@ -78,7 +145,7 @@ class _TaskPageState extends State<TaskPage> {
|
||||
child: RefreshIndicator(
|
||||
color: const Color(0xFF16DBB2),
|
||||
backgroundColor: const Color(0xFF06342A),
|
||||
onRefresh: _loadTasks,
|
||||
onRefresh: _handleManualRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
@ -122,21 +189,24 @@ class _TaskPageState extends State<TaskPage> {
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
child:
|
||||
widget.showBackButton
|
||||
? GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(width: 44.w, height: 44.w),
|
||||
),
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.task,
|
||||
@ -209,6 +279,21 @@ class _TaskPageState extends State<TaskPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (sections.isEmpty) {
|
||||
sections.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 120.w),
|
||||
child: Text(
|
||||
l10n.noPromptsToday,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
@ -218,29 +303,19 @@ class _TaskPageState extends State<TaskPage> {
|
||||
final taskType = (task.taskType ?? 0).toInt();
|
||||
return isNewcomer ? taskType != 0 : taskType == 0;
|
||||
}).toList();
|
||||
final mapped = source.map(_mapTask).toList();
|
||||
if (_tasks.isNotEmpty) return mapped;
|
||||
final demoTasks = _demoTasks(isNewcomer: isNewcomer);
|
||||
return demoTasks
|
||||
.map(
|
||||
(task) =>
|
||||
_claimedTaskIds.contains(task.id)
|
||||
? task.copyWith(status: _TaskActionStatus.claimed)
|
||||
: task,
|
||||
)
|
||||
.toList();
|
||||
return source.map(_mapTask).toList();
|
||||
}
|
||||
|
||||
_TaskUiItem _mapTask(SCTaskListRes task) {
|
||||
final id = task.taskId ?? -1;
|
||||
final id = task.taskId ?? '';
|
||||
final currentValue = _parseNumber(task.completedValue) ?? 0;
|
||||
final targetValue =
|
||||
_parseNumber(task.targetValue) ??
|
||||
_parseNumber(task.conditionValue) ??
|
||||
1000000;
|
||||
final parsedTargetValue =
|
||||
_parseNumber(task.targetValue) ?? _parseNumber(task.conditionValue);
|
||||
final targetValue = parsedTargetValue ?? 0;
|
||||
final hasProgress = parsedTargetValue != null && parsedTargetValue > 0;
|
||||
final claimed =
|
||||
_claimedTaskIds.contains(id) || (task.isRewardCollected ?? 0) == 1;
|
||||
final completedByValue = targetValue > 0 && currentValue >= targetValue;
|
||||
final completedByValue = hasProgress && currentValue >= targetValue;
|
||||
final completedByStatus = (task.taskStatus ?? 0) == 1;
|
||||
final status =
|
||||
claimed
|
||||
@ -254,27 +329,46 @@ class _TaskPageState extends State<TaskPage> {
|
||||
title:
|
||||
_nonEmpty(task.taskName) ??
|
||||
SCAppLocalizations.of(context)!.useMicrophoneForOneMin,
|
||||
progressText:
|
||||
'${_formatPlainNumber(currentValue)}/${_formatPlainNumber(targetValue)}',
|
||||
rewardText: _formatCompactNumber(task.quantity ?? 1100),
|
||||
progressText: _taskProgressText(
|
||||
task,
|
||||
currentValue: currentValue,
|
||||
targetValue: targetValue,
|
||||
hasProgress: hasProgress,
|
||||
),
|
||||
requirementText: _taskRequirementText(
|
||||
task,
|
||||
targetValue: targetValue,
|
||||
hasProgress: hasProgress,
|
||||
),
|
||||
rewardText: _formatPlainNumber(task.quantity ?? 1100),
|
||||
status: status,
|
||||
iconAsset: _iconForTask(task),
|
||||
iconUrl: _nonEmpty(task.taskIcon) ?? _nonEmpty(task.cover),
|
||||
jumpPage: _nonEmpty(task.jumpPage),
|
||||
fromApi: task.taskId != null,
|
||||
jumpType: _nonEmpty(task.jumpType),
|
||||
conditionType: _nonEmpty(task.conditionType),
|
||||
roomId: _nonEmpty(task.roomId),
|
||||
roomTask: _isRoomCompletionTask(task),
|
||||
gameTask: _isGameCompletionTask(task),
|
||||
fromApi: id.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleTaskAction(_TaskUiItem task) async {
|
||||
if (task.status == _TaskActionStatus.claimed) return;
|
||||
if (task.status == _TaskActionStatus.go) {
|
||||
_openJumpPage(task.jumpPage);
|
||||
final opened = await _openJumpPage(task);
|
||||
if (opened && mounted) {
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('jump return');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_claimingTaskIds.contains(task.id)) return;
|
||||
setState(() => _claimingTaskIds.add(task.id));
|
||||
var success = true;
|
||||
if (task.fromApi && task.id >= 0) {
|
||||
if (task.fromApi && task.id.isNotEmpty) {
|
||||
try {
|
||||
success = await _repository.taskReward(task.id);
|
||||
} catch (_) {
|
||||
@ -294,19 +388,124 @@ class _TaskPageState extends State<TaskPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _openJumpPage(String? jumpPage) {
|
||||
final target = jumpPage?.trim() ?? '';
|
||||
if (target.isEmpty) return;
|
||||
if (target.startsWith('http://') || target.startsWith('https://')) {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
'${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(target)}&showTitle=false',
|
||||
);
|
||||
return;
|
||||
Future<bool> _openJumpPage(_TaskUiItem task) async {
|
||||
var target = _taskJumpUrl(task);
|
||||
if (target.isEmpty) return false;
|
||||
var uri = Uri.tryParse(target);
|
||||
var path = uri?.path ?? '';
|
||||
var roomId = uri?.queryParameters['roomId']?.trim() ?? '';
|
||||
if ((path == '/room' || path == '/game-center') && roomId.isEmpty) {
|
||||
final fallbackTarget = await _firstRoomJumpUrl(target);
|
||||
if (!mounted) return false;
|
||||
if (fallbackTarget.isEmpty) {
|
||||
_showToast(SCAppLocalizations.of(context)!.enterRoomFailedRetry);
|
||||
return false;
|
||||
}
|
||||
target = fallbackTarget;
|
||||
uri = Uri.tryParse(target);
|
||||
path = uri?.path ?? '';
|
||||
roomId = uri?.queryParameters['roomId']?.trim() ?? '';
|
||||
}
|
||||
if (target.startsWith('/')) {
|
||||
SCNavigatorUtils.push(context, target);
|
||||
|
||||
final jumpType = _taskJumpType(task, target);
|
||||
await SCEntryPopupCoordinator.openConfiguredJump(
|
||||
context,
|
||||
jumpType: jumpType,
|
||||
jumpUrl: target,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> _firstRoomJumpUrl(String target) async {
|
||||
try {
|
||||
final rooms = await SCChatRoomRepository().discovery(allRegion: true);
|
||||
for (final room in rooms) {
|
||||
final roomId = room.id?.trim() ?? '';
|
||||
if (roomId.isEmpty) continue;
|
||||
final resolvedTarget = _withRoomId(target, roomId);
|
||||
return resolvedTarget;
|
||||
}
|
||||
} catch (error) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _withRoomId(String target, String roomId) {
|
||||
final uri = Uri.tryParse(target);
|
||||
if (uri == null) {
|
||||
return '/room?roomId=${Uri.encodeComponent(roomId)}';
|
||||
}
|
||||
final params = Map<String, String>.from(uri.queryParameters);
|
||||
params['roomId'] = roomId;
|
||||
return uri.replace(queryParameters: params).toString();
|
||||
}
|
||||
|
||||
String _taskJumpUrl(_TaskUiItem task) {
|
||||
final target = task.jumpPage?.trim() ?? '';
|
||||
final roomId = task.roomId?.trim() ?? '';
|
||||
if (target.isEmpty) {
|
||||
return _defaultTaskJumpUrl(task, roomId);
|
||||
}
|
||||
if (roomId.isEmpty) return target;
|
||||
|
||||
final uri = Uri.tryParse(target);
|
||||
if (uri == null) return target;
|
||||
if (uri.path != '/room' && uri.path != '/game-center') return target;
|
||||
return _withRoomId(target, roomId);
|
||||
}
|
||||
|
||||
String _defaultTaskJumpUrl(_TaskUiItem task, String roomId) {
|
||||
if (_isGameJumpType(task.jumpType) || task.gameTask) {
|
||||
return _routeWithOptionalRoomId('/game-center', roomId);
|
||||
}
|
||||
if (_isRoomJumpType(task.jumpType) || task.roomTask) {
|
||||
return _routeWithOptionalRoomId('/room', roomId);
|
||||
}
|
||||
if (_isRechargeTask(task)) {
|
||||
return '/recharge';
|
||||
}
|
||||
return roomId.isEmpty ? '' : _routeWithOptionalRoomId('/room', roomId);
|
||||
}
|
||||
|
||||
String _routeWithOptionalRoomId(String route, String roomId) {
|
||||
if (roomId.isEmpty) return route;
|
||||
return '$route?roomId=${Uri.encodeComponent(roomId)}';
|
||||
}
|
||||
|
||||
String _taskJumpType(_TaskUiItem task, String target) {
|
||||
final uri = Uri.tryParse(target);
|
||||
final path = uri?.path ?? '';
|
||||
final jumpType = task.jumpType?.trim();
|
||||
if (jumpType != null && jumpType.isNotEmpty) {
|
||||
if (_isGameJumpType(jumpType)) return 'GAME';
|
||||
if (_isRoomJumpType(jumpType)) return 'APP_ROUTE';
|
||||
}
|
||||
if (uri?.hasScheme == true) {
|
||||
return 'H5';
|
||||
}
|
||||
if (path == '/game-center') {
|
||||
return 'GAME';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
return 'APP_ROUTE';
|
||||
}
|
||||
if (jumpType != null && jumpType.isNotEmpty) return jumpType;
|
||||
return 'APP_ROUTE';
|
||||
}
|
||||
|
||||
bool _isRoomJumpType(String? jumpType) {
|
||||
final normalized = jumpType?.trim().toUpperCase() ?? '';
|
||||
return normalized == 'ROOM' ||
|
||||
normalized == 'VOICE_ROOM' ||
|
||||
normalized == 'VOICECHAT_ROOM' ||
|
||||
normalized == 'ENTER_ROOM' ||
|
||||
normalized == 'JOIN_ROOM';
|
||||
}
|
||||
|
||||
bool _isGameJumpType(String? jumpType) {
|
||||
final normalized = jumpType?.trim().toUpperCase() ?? '';
|
||||
return normalized == 'GAME' ||
|
||||
normalized == 'GAME_CENTER' ||
|
||||
normalized == 'GAME_CONSUME_GOLD';
|
||||
}
|
||||
|
||||
void _showToast(String message) {
|
||||
@ -318,17 +517,82 @@ class _TaskPageState extends State<TaskPage> {
|
||||
}
|
||||
|
||||
String _iconForTask(SCTaskListRes task) {
|
||||
return _taskGiftAsset;
|
||||
}
|
||||
|
||||
String _taskProgressText(
|
||||
SCTaskListRes task, {
|
||||
required num currentValue,
|
||||
required num targetValue,
|
||||
required bool hasProgress,
|
||||
}) {
|
||||
if (!hasProgress) return '';
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
return '${l10n.taskProgress}: '
|
||||
'${_formatTaskValue(task, currentValue)}/${_formatTaskValue(task, targetValue)}';
|
||||
}
|
||||
|
||||
String _taskRequirementText(
|
||||
SCTaskListRes task, {
|
||||
required num targetValue,
|
||||
required bool hasProgress,
|
||||
}) {
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
if (hasProgress) {
|
||||
return '${l10n.taskRequirement}: ${_formatTaskValue(task, targetValue)}';
|
||||
}
|
||||
return _nonEmpty(task.taskDesc) ?? '';
|
||||
}
|
||||
|
||||
bool _isGameCompletionTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('GAME')) return true;
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
return text.contains('game') || text.contains('游戏');
|
||||
}
|
||||
|
||||
bool _isRoomCompletionTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('RECHARGE')) return false;
|
||||
if (_isGameCompletionTask(task)) return false;
|
||||
if (conditionType.contains('MIC') ||
|
||||
conditionType.contains('VOICE') ||
|
||||
conditionType.contains('ROOM') ||
|
||||
conditionType.contains('GIFT') ||
|
||||
conditionType.contains('CONSUME')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
if (text.contains('recharge') || text.contains('充值')) return false;
|
||||
if (text.contains('game') || text.contains('游戏')) return false;
|
||||
return text.contains('mic') ||
|
||||
text.contains('voice') ||
|
||||
text.contains('room') ||
|
||||
text.contains('gift') ||
|
||||
text.contains('consume') ||
|
||||
text.contains('spend') ||
|
||||
text.contains('上麦') ||
|
||||
text.contains('房间') ||
|
||||
text.contains('语音') ||
|
||||
text.contains('礼物') ||
|
||||
text.contains('消费');
|
||||
}
|
||||
|
||||
bool _isRechargeTask(_TaskUiItem task) {
|
||||
final text =
|
||||
'${task.conditionType ?? ''} ${task.taskName ?? ''} ${task.taskDesc ?? ''}'
|
||||
'${task.conditionType ?? ''} ${task.title} ${task.jumpType ?? ''}'
|
||||
.toLowerCase();
|
||||
if (text.contains('gift')) return _taskGiftAsset;
|
||||
if (text.contains('game')) return _taskGameAsset;
|
||||
return _taskMicAsset;
|
||||
return text.contains('recharge') || text.contains('充值');
|
||||
}
|
||||
|
||||
static num? _parseNumber(Object? value) {
|
||||
if (value is num) return value;
|
||||
if (value is String) return num.tryParse(value);
|
||||
if (value is String) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) return null;
|
||||
return num.tryParse(text) ?? num.tryParse(text.replaceAll(',', ''));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -342,51 +606,65 @@ class _TaskPageState extends State<TaskPage> {
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
static String _formatCompactNumber(num value) {
|
||||
final absValue = value.abs();
|
||||
if (absValue >= 1000000) {
|
||||
return '${_trimTrailingZero(value / 1000000)}m';
|
||||
}
|
||||
if (absValue >= 1000) {
|
||||
return '${_trimTrailingZero(value / 1000)}k';
|
||||
}
|
||||
String _formatTaskValue(SCTaskListRes task, num value) {
|
||||
if (_isTimeValueTask(task)) return _formatDurationSeconds(value);
|
||||
return _formatPlainNumber(value);
|
||||
}
|
||||
|
||||
static String _trimTrailingZero(num value) {
|
||||
final text = value.toStringAsFixed(1);
|
||||
return text.endsWith('.0') ? text.substring(0, text.length - 2) : text;
|
||||
bool _isTimeValueTask(SCTaskListRes task) {
|
||||
final conditionType = task.conditionType?.trim().toUpperCase() ?? '';
|
||||
if (conditionType.contains('GOLD') || conditionType.contains('CONSUME')) {
|
||||
return false;
|
||||
}
|
||||
if (conditionType.contains('SECOND') ||
|
||||
conditionType.contains('DURATION') ||
|
||||
RegExp(r'(^|_)TIME($|_)').hasMatch(conditionType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase();
|
||||
return text.contains('second') ||
|
||||
text.contains('seconds') ||
|
||||
text.contains('minute') ||
|
||||
text.contains('minutes') ||
|
||||
text.contains('hour') ||
|
||||
text.contains('hours') ||
|
||||
text.contains(' min') ||
|
||||
text.contains('秒') ||
|
||||
text.contains('分钟') ||
|
||||
text.contains('小时');
|
||||
}
|
||||
|
||||
List<_TaskUiItem> _demoTasks({required bool isNewcomer}) {
|
||||
final title = SCAppLocalizations.of(context)!.useMicrophoneForOneMin;
|
||||
final baseId = isNewcomer ? -200 : -100;
|
||||
return [
|
||||
_TaskUiItem(
|
||||
id: baseId - 1,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.go,
|
||||
iconAsset: _taskMicAsset,
|
||||
),
|
||||
_TaskUiItem(
|
||||
id: baseId - 2,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.claim,
|
||||
iconAsset: _taskGameAsset,
|
||||
),
|
||||
_TaskUiItem(
|
||||
id: baseId - 3,
|
||||
title: title,
|
||||
progressText: '0/1000000',
|
||||
rewardText: '1.1k',
|
||||
status: _TaskActionStatus.claimed,
|
||||
iconAsset: _taskGiftAsset,
|
||||
),
|
||||
];
|
||||
static String _formatDurationSeconds(num seconds) {
|
||||
final absSeconds = seconds.abs();
|
||||
if (absSeconds >= 3600) {
|
||||
return '${_formatShortDecimal(seconds / 3600)}h';
|
||||
}
|
||||
if (absSeconds >= 60) {
|
||||
return '${_formatShortDecimal(seconds / 60)}min';
|
||||
}
|
||||
return '${_formatShortDecimal(seconds)}s';
|
||||
}
|
||||
|
||||
static String _formatShortDecimal(num value) {
|
||||
final fixed = value.toStringAsFixed(1);
|
||||
return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed;
|
||||
}
|
||||
|
||||
String _taskDebugSample(List<SCTaskListRes> tasks) {
|
||||
if (tasks.isEmpty) return 'empty';
|
||||
return tasks
|
||||
.take(3)
|
||||
.map((task) {
|
||||
return '{id=${task.taskId},type=${task.taskType},'
|
||||
'status=${task.taskStatus},collected=${task.isRewardCollected},'
|
||||
'current=${task.completedValue},target=${task.targetValue},'
|
||||
'quantity=${task.quantity},name=${task.taskName},'
|
||||
'condition=${task.conditionType},'
|
||||
'jumpType=${task.jumpType},jumpPage=${task.jumpPage},'
|
||||
'roomId=${task.roomId}}';
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,7 +681,7 @@ class _TaskSection extends StatelessWidget {
|
||||
final String subtitle;
|
||||
final List<_TaskUiItem> tasks;
|
||||
final ValueChanged<_TaskUiItem> onAction;
|
||||
final bool Function(num id) isClaiming;
|
||||
final bool Function(String id) isClaiming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -552,7 +830,7 @@ class _TaskRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 65.w,
|
||||
height: 76.w,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 12.w),
|
||||
@ -564,12 +842,7 @@ class _TaskRow extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset(
|
||||
task.iconAsset,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
child: _TaskIcon(task: task),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Expanded(
|
||||
@ -589,18 +862,35 @@ class _TaskRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
if (task.requirementText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.requirementText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
SizedBox(height: 3.w),
|
||||
],
|
||||
if (task.progressText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
] else
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
@ -697,6 +987,40 @@ class _TaskActionButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskIcon extends StatelessWidget {
|
||||
const _TaskIcon({required this.task});
|
||||
|
||||
final _TaskUiItem task;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconUrl = task.iconUrl?.trim();
|
||||
if (iconUrl == null || iconUrl.isEmpty) {
|
||||
return _buildAssetIcon();
|
||||
}
|
||||
return Image.network(
|
||||
iconUrl,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _buildAssetIcon(),
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return _buildAssetIcon();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetIcon() {
|
||||
return Image.asset(
|
||||
task.iconAsset,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskButtonConfig {
|
||||
const _TaskButtonConfig({
|
||||
required this.label,
|
||||
@ -755,20 +1079,34 @@ class _TaskUiItem {
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.progressText,
|
||||
required this.requirementText,
|
||||
required this.rewardText,
|
||||
required this.status,
|
||||
required this.iconAsset,
|
||||
this.iconUrl,
|
||||
this.jumpPage,
|
||||
this.jumpType,
|
||||
this.conditionType,
|
||||
this.roomId,
|
||||
this.roomTask = false,
|
||||
this.gameTask = false,
|
||||
this.fromApi = false,
|
||||
});
|
||||
|
||||
final num id;
|
||||
final String id;
|
||||
final String title;
|
||||
final String progressText;
|
||||
final String requirementText;
|
||||
final String rewardText;
|
||||
final _TaskActionStatus status;
|
||||
final String iconAsset;
|
||||
final String? iconUrl;
|
||||
final String? jumpPage;
|
||||
final String? jumpType;
|
||||
final String? conditionType;
|
||||
final String? roomId;
|
||||
final bool roomTask;
|
||||
final bool gameTask;
|
||||
final bool fromApi;
|
||||
|
||||
_TaskUiItem copyWith({_TaskActionStatus? status}) {
|
||||
@ -776,10 +1114,17 @@ class _TaskUiItem {
|
||||
id: id,
|
||||
title: title,
|
||||
progressText: progressText,
|
||||
requirementText: requirementText,
|
||||
rewardText: rewardText,
|
||||
status: status ?? this.status,
|
||||
iconAsset: iconAsset,
|
||||
iconUrl: iconUrl,
|
||||
jumpPage: jumpPage,
|
||||
jumpType: jumpType,
|
||||
conditionType: conditionType,
|
||||
roomId: roomId,
|
||||
roomTask: roomTask,
|
||||
gameTask: gameTask,
|
||||
fromApi: fromApi,
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
final results = await Future.wait<dynamic>([
|
||||
_repository.vipHome(),
|
||||
_repository.vipStatus().catchError((error) {
|
||||
debugPrint('VIP status load failed: $error');
|
||||
return SCVipStatusRes();
|
||||
}),
|
||||
]);
|
||||
@ -155,11 +154,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
if (_home?.configured == false ||
|
||||
config?.enabled == false ||
|
||||
config?.canPurchase != true) {
|
||||
debugPrint(
|
||||
'[VIP][Page] skip preview level=$level '
|
||||
'configured=${_home?.configured} enabled=${config?.enabled} '
|
||||
'canPurchase=${config?.canPurchase}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -201,17 +195,12 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
|
||||
if (!mounted) return;
|
||||
if (purchase.success == true) {
|
||||
debugPrint(
|
||||
'[VIP][Page] purchase success targetLevel=$_selectedLevel '
|
||||
'orderId=${purchase.order?.id} paidGold=${purchase.order?.paidGold}',
|
||||
);
|
||||
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
profileManager.balance();
|
||||
await _loadVipData(showLoading: false);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('VIP purchase failed: $error');
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
if (mounted) {
|
||||
@ -230,24 +219,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
SCVipStatusRes? status,
|
||||
int selectedLevel,
|
||||
) {
|
||||
debugPrint(
|
||||
'[VIP][Page] home loaded configured=${home.configured} '
|
||||
'levels=${home.levels.length} selectedLevel=$selectedLevel '
|
||||
'statusActive=${status?.active} statusLevel=${status?.level} '
|
||||
'statusExpireAt=${status?.expireAt}',
|
||||
);
|
||||
if (home.configured != true) {
|
||||
debugPrint(
|
||||
'[VIP][Page][Warn] vip home configured=${home.configured}; '
|
||||
'check backend VIP config for sysOrigin=${home.sysOrigin}',
|
||||
);
|
||||
}
|
||||
if (home.levels.isEmpty) {
|
||||
debugPrint(
|
||||
'[VIP][Page][Warn] vip home levels is empty; '
|
||||
'frontend has no level data to render.',
|
||||
);
|
||||
}
|
||||
if (home.configured != true) {}
|
||||
if (home.levels.isEmpty) {}
|
||||
_logSelectedLevelResources(
|
||||
selectedLevel,
|
||||
config: _levelConfigFromHome(home, selectedLevel),
|
||||
@ -262,9 +235,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
}) {
|
||||
final state = status?.levelInt == level ? status : null;
|
||||
if (config == null && state == null) {
|
||||
debugPrint(
|
||||
'[VIP][Page][Warn] selected level=$level has no config or state',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final badge = _preferResource(config?.badge, state?.badge);
|
||||
@ -281,14 +251,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
config?.floatPicture,
|
||||
state?.floatPicture,
|
||||
);
|
||||
debugPrint(
|
||||
'[VIP][Page] selected level resources level=$level '
|
||||
'badge=${_resourceLogValue(badge)} '
|
||||
'avatarFrame=${_resourceLogValue(avatarFrame)} '
|
||||
'entryEffect=${_resourceLogValue(entryEffect)} '
|
||||
'chatBubble=${_resourceLogValue(chatBubble)} '
|
||||
'floatPicture=${_resourceLogValue(floatPicture)}',
|
||||
);
|
||||
}
|
||||
|
||||
String _resourceLogValue(SCVipResourceRes? resource) {
|
||||
@ -517,17 +479,20 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
config?.displayNameText ?? 'VIP$level',
|
||||
style: TextStyle(
|
||||
color:
|
||||
selected
|
||||
? Colors.white
|
||||
: Colors.white.withValues(
|
||||
alpha: enabled ? 0.42 : 0.20,
|
||||
),
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text(
|
||||
config?.displayNameText ?? 'VIP$level',
|
||||
style: TextStyle(
|
||||
color:
|
||||
selected
|
||||
? Colors.white
|
||||
: Colors.white.withValues(
|
||||
alpha: enabled ? 0.42 : 0.20,
|
||||
),
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
@ -1147,12 +1112,15 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
_priceText,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFE8A7),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text(
|
||||
_priceText,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFE8A7),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
@ -1208,31 +1176,34 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_v.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_i.png',
|
||||
width: 14.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_p.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
_buildVipLevelNumber(level),
|
||||
],
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_v.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_i.png',
|
||||
width: 14.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.asset(
|
||||
'sc_images/vip/sc_vip_letter_p.png',
|
||||
width: 28.w,
|
||||
height: 24.7.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
_buildVipLevelNumber(level),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1362,7 +1333,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
preview?.payableGold ?? config?.payableGold ?? config?.priceGold;
|
||||
final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt();
|
||||
final amountText = amount == null ? '--' : _formatGold(amount);
|
||||
return '$amountText / $days Days';
|
||||
final daysLabel = SCAppLocalizations.of(context)?.days ?? 'Days';
|
||||
return '$amountText / $days $daysLabel';
|
||||
}
|
||||
|
||||
String _formatGold(num value) {
|
||||
|
||||
@ -64,6 +64,7 @@ class _VisitorUserListPageState
|
||||
url: res.userProfile?.userAvatar ?? "",
|
||||
width: 55.w,
|
||||
headdress: res.userProfile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: res.userProfile?.getHeaddress()?.cover,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Expanded(
|
||||
@ -79,17 +80,17 @@ class _VisitorUserListPageState
|
||||
textColor: Colors.white,
|
||||
type: res.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
14,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
res.userProfile?.getVIP() != null
|
||||
? netImage(
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
url: res.userProfile?.getVIP()?.cover ?? "",
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
|
||||
@ -230,9 +230,7 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
_title = title.replaceAll("\"", "");
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print("获取标题失败: $e");
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -312,7 +310,6 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
}
|
||||
|
||||
void _onWebResourceError(WebResourceError error) {
|
||||
print('_onWebResourceError:${error.description}');
|
||||
// 可以在这里添加错误处理逻辑,比如显示错误页面
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
31
lib/services/audio/voice_room_foreground_service.dart
Normal file
31
lib/services/audio/voice_room_foreground_service.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SCVoiceRoomForegroundService {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'com.org.yumiparty/voice_room_foreground_service',
|
||||
);
|
||||
|
||||
static Future<void> start({String? roomName}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _channel.invokeMethod<void>('start', {
|
||||
'roomName': roomName?.trim(),
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
static Future<void> stop() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _channel.invokeMethod<void>('stop');
|
||||
} on PlatformException catch (e) {
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
@ -148,8 +148,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _showAuthError(Object error) {
|
||||
debugPrint("authenticateUser error: $error");
|
||||
|
||||
if (error is DioException) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -225,9 +225,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().inviteHost(id, status);
|
||||
getUserIdentity();
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('inviteHostOpt failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void inviteAgentOpt(BuildContext ct, String id, String status) async {
|
||||
@ -236,9 +234,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().inviteAgent(id, status);
|
||||
getUserIdentity();
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('inviteAgentOpt failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void inviteBDOpt(BuildContext ct, String id, String status) async {
|
||||
@ -247,9 +243,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().inviteBD(id, status);
|
||||
getUserIdentity();
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('inviteBDOpt failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void inviteBDLeader(BuildContext ct, String id, String status) async {
|
||||
@ -258,9 +252,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().inviteBDLeader(id, status);
|
||||
getUserIdentity();
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('inviteBDLeader failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void inviteRechargeAgent(BuildContext ct, String id, String status) async {
|
||||
@ -269,9 +261,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().inviteRechargeAgent(id, status);
|
||||
getUserIdentity();
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('inviteRechargeAgent failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void cpRlationshipProcessApply(
|
||||
@ -284,8 +274,6 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
await SCAccountRepository().cpRelationshipProcessApply(id, status);
|
||||
fetchUserProfileData(loadGuardCount: false);
|
||||
SCTts.show(successMessage);
|
||||
} catch (e) {
|
||||
debugPrint('cpRlationshipProcessApply failed: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||
|
||||
class GiftAnimationManager extends ChangeNotifier {
|
||||
static const int maxVisibleSlots = 4;
|
||||
static const int _maxPendingAnimations = 24;
|
||||
static const Duration _activeComboIdleDismissDelay = Duration(
|
||||
milliseconds: 3200,
|
||||
@ -17,10 +18,16 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
//每个控件正在播放的动画
|
||||
Map<int, LGiftModel?> giftMap = {0: null, 1: null, 2: null, 3: null};
|
||||
|
||||
int _visibleSlotCount = 0;
|
||||
|
||||
bool get _controllersReady =>
|
||||
animationControllerList.length >= giftMap.length;
|
||||
_visibleSlotCount <= 0 ||
|
||||
animationControllerList.length >= _visibleSlotCount;
|
||||
|
||||
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
|
||||
if (index < 0 || index >= _visibleSlotCount) {
|
||||
return null;
|
||||
}
|
||||
final gift = giftMap[index];
|
||||
if (gift == null || animationControllerList.length <= index) {
|
||||
return null;
|
||||
@ -43,6 +50,31 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void setVisibleSlotCount(int count) {
|
||||
final nextCount = count.clamp(0, maxVisibleSlots).toInt();
|
||||
if (nextCount == _visibleSlotCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldCount = _visibleSlotCount;
|
||||
if (nextCount < oldCount) {
|
||||
for (var index = oldCount - 1; index >= nextCount; index -= 1) {
|
||||
final gift = giftMap[index];
|
||||
if (gift == null) {
|
||||
continue;
|
||||
}
|
||||
_cancelSlotDismissTimer(index);
|
||||
giftMap[index] = null;
|
||||
pendingAnimationsQueue.addFirst(gift);
|
||||
}
|
||||
_trimPendingAnimationsFromEnd();
|
||||
}
|
||||
|
||||
_visibleSlotCount = nextCount;
|
||||
notifyListeners();
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void enqueueGiftAnimation(LGiftModel giftModel) {
|
||||
if (_mergeIntoActiveAnimation(giftModel)) {
|
||||
return;
|
||||
@ -55,21 +87,42 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void clearActiveAnimations() {
|
||||
pendingAnimationsQueue.clear();
|
||||
var changed = false;
|
||||
for (final index in giftMap.keys.toList()) {
|
||||
_cancelSlotDismissTimer(index);
|
||||
if (giftMap[index] != null) {
|
||||
giftMap[index] = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _trimPendingAnimations() {
|
||||
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void _trimPendingAnimationsFromEnd() {
|
||||
while (pendingAnimationsQueue.length > _maxPendingAnimations) {
|
||||
pendingAnimationsQueue.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
|
||||
for (final entry in giftMap.entries) {
|
||||
final current = entry.value;
|
||||
for (var index = 0; index < _visibleSlotCount; index += 1) {
|
||||
final current = giftMap[index];
|
||||
if (current == null || current.labelId != incoming.labelId) {
|
||||
continue;
|
||||
}
|
||||
_mergeGiftModel(target: current, incoming: incoming);
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(entry.key, restartEntry: false);
|
||||
_refreshSlotAnimation(index, restartEntry: false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -119,41 +172,60 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
|
||||
///开始播放
|
||||
proceedToNextAnimation() {
|
||||
if (pendingAnimationsQueue.isEmpty || !_controllersReady) {
|
||||
if (pendingAnimationsQueue.isEmpty ||
|
||||
!_controllersReady ||
|
||||
_visibleSlotCount <= 0) {
|
||||
return;
|
||||
}
|
||||
var playGift = pendingAnimationsQueue.first;
|
||||
for (var key in giftMap.keys) {
|
||||
var value = giftMap[key];
|
||||
if (value == null) {
|
||||
giftMap[key] = playGift;
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(key, restartEntry: true);
|
||||
break;
|
||||
} else {
|
||||
|
||||
var changed = false;
|
||||
while (pendingAnimationsQueue.isNotEmpty) {
|
||||
final playGift = pendingAnimationsQueue.first;
|
||||
var consumed = false;
|
||||
for (var index = 0; index < _visibleSlotCount; index += 1) {
|
||||
final value = giftMap[index];
|
||||
if (value == null) {
|
||||
giftMap[index] = playGift;
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
changed = true;
|
||||
consumed = true;
|
||||
_refreshSlotAnimation(index, restartEntry: true);
|
||||
break;
|
||||
}
|
||||
if (value.labelId == playGift.labelId) {
|
||||
_mergeGiftModel(target: value, incoming: playGift);
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(key, restartEntry: false);
|
||||
changed = true;
|
||||
consumed = true;
|
||||
_refreshSlotAnimation(index, restartEntry: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!consumed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void attachAnimationControllers(List<LGiftScrollingScreenAnimsBean> anins) {
|
||||
animationControllerList = anins;
|
||||
if (_visibleSlotCount > animationControllerList.length) {
|
||||
setVisibleSlotCount(animationControllerList.length);
|
||||
return;
|
||||
}
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void cleanupAnimationResources() {
|
||||
pendingAnimationsQueue.clear();
|
||||
giftMap[0] = null;
|
||||
giftMap[1] = null;
|
||||
giftMap[2] = null;
|
||||
giftMap[3] = null;
|
||||
for (final key in giftMap.keys) {
|
||||
giftMap[key] = null;
|
||||
}
|
||||
_visibleSlotCount = 0;
|
||||
for (var element in animationControllerList) {
|
||||
element.dismissTimer?.cancel();
|
||||
element.controller.dispose();
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
@ -23,7 +22,6 @@ class SCHomeRoomPreloadManager {
|
||||
}
|
||||
unawaited(
|
||||
loadPartyRooms().catchError((error, stackTrace) {
|
||||
debugPrint('[HomePreload] party rooms preload failed: $error');
|
||||
return <SocialChatRoomRes>[];
|
||||
}),
|
||||
);
|
||||
|
||||
@ -13,6 +13,10 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
enum RoomMusicPlayMode { list, shuffle, single }
|
||||
|
||||
class RoomMusicManager extends ChangeNotifier {
|
||||
static const Duration _publishingStateHeartbeatInterval = Duration(
|
||||
seconds: 5,
|
||||
);
|
||||
|
||||
RoomMusicManager({RoomMusicRepository? repository})
|
||||
: _repository = repository ?? RoomMusicRepository() {
|
||||
_playlist = _repository.loadSongs();
|
||||
@ -23,6 +27,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
|
||||
RtcProvider? _rtcProvider;
|
||||
Timer? _positionTimer;
|
||||
Timer? _publishingStateHeartbeatTimer;
|
||||
List<SCMusicMode> _playlist = <SCMusicMode>[];
|
||||
SCMusicMode? _current;
|
||||
RoomMusicPlayMode _playMode = RoomMusicPlayMode.list;
|
||||
@ -35,6 +40,8 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
bool _isPaused = false;
|
||||
bool _isStarting = false;
|
||||
bool _isStoppingByUser = false;
|
||||
bool _isRoutingSwitching = false;
|
||||
bool _lastPublishedRoomState = false;
|
||||
bool _lastLoopback = true;
|
||||
bool _roomPlayerVisible = false;
|
||||
|
||||
@ -59,7 +66,11 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
|
||||
bool get roomPlayerVisible => _roomPlayerVisible;
|
||||
|
||||
bool get isPublishingToRoom => _isPlaying && !_lastLoopback;
|
||||
bool get isPublishingToRoom =>
|
||||
_isPlaying &&
|
||||
!_isPaused &&
|
||||
!_lastLoopback &&
|
||||
(_rtcProvider?.isOnMai() ?? false);
|
||||
|
||||
int get currentIndex {
|
||||
final currentId = _current?.id;
|
||||
@ -78,6 +89,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_rtcProvider = provider;
|
||||
provider.setRoomMusicMixingStateListener(_handleMixingStateChanged);
|
||||
provider.setRoomMusicMicRouteChangedListener(_handleMicRouteChanged);
|
||||
_syncPublishingState(force: true);
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
@ -134,7 +146,11 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
await _savePlaylist(next);
|
||||
}
|
||||
|
||||
Future<void> play(SCMusicMode item, {int startPositionMs = 0}) async {
|
||||
Future<void> play(
|
||||
SCMusicMode item, {
|
||||
int startPositionMs = 0,
|
||||
bool showRoomPlayer = true,
|
||||
}) async {
|
||||
if (item.playPath.isEmpty) {
|
||||
SCTts.show("Music file not found");
|
||||
return;
|
||||
@ -149,7 +165,9 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_durationMs = item.durationMs;
|
||||
_positionMs = startPositionMs;
|
||||
_isPaused = false;
|
||||
_roomPlayerVisible = true;
|
||||
if (showRoomPlayer) {
|
||||
_roomPlayerVisible = true;
|
||||
}
|
||||
await _startCurrent(startPositionMs: startPositionMs);
|
||||
}
|
||||
|
||||
@ -161,6 +179,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_isPlaying = false;
|
||||
_isPaused = true;
|
||||
_stopPositionTimer();
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -169,10 +188,25 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
if (_isPaused) {
|
||||
final nextLoopback = !_rtcProvider!.isOnMai();
|
||||
if (nextLoopback != _lastLoopback) {
|
||||
final wasRoomPlayerVisible = _roomPlayerVisible;
|
||||
_ignoreStoppedUntilMs = DateTime.now().millisecondsSinceEpoch + 900;
|
||||
_isRoutingSwitching = true;
|
||||
try {
|
||||
await _rtcProvider?.stopRoomMusicAudioMixing();
|
||||
await _startCurrent(startPositionMs: _positionMs);
|
||||
_roomPlayerVisible = wasRoomPlayerVisible;
|
||||
} finally {
|
||||
_isRoutingSwitching = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _rtcProvider?.resumeRoomMusicAudioMixing();
|
||||
_isPlaying = true;
|
||||
_isPaused = false;
|
||||
_startPositionTimer();
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
@ -196,6 +230,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_durationMs = 0;
|
||||
_roomPlayerVisible = false;
|
||||
}
|
||||
_syncPublishingState(force: clearCurrent);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -219,7 +254,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
if (_playMode == RoomMusicPlayMode.single && fromAutoComplete) {
|
||||
await play(_current ?? _playlist.first);
|
||||
await play(_current ?? _playlist.first, showRoomPlayer: false);
|
||||
return;
|
||||
}
|
||||
int targetIndex = 0;
|
||||
@ -235,7 +270,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
} else {
|
||||
targetIndex = index < 0 ? 0 : (index + 1) % _playlist.length;
|
||||
}
|
||||
await play(_playlist[targetIndex]);
|
||||
await play(_playlist[targetIndex], showRoomPlayer: !fromAutoComplete);
|
||||
}
|
||||
|
||||
Future<void> seek(int positionMs) async {
|
||||
@ -298,13 +333,19 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
.getRoomMusicAudioMixingPosition()
|
||||
.catchError((_) => _positionMs);
|
||||
_ignoreStoppedUntilMs = DateTime.now().millisecondsSinceEpoch + 900;
|
||||
final wasRoomPlayerVisible = _roomPlayerVisible;
|
||||
_isRoutingSwitching = true;
|
||||
try {
|
||||
await rtcProvider.stopRoomMusicAudioMixing();
|
||||
await _startCurrent(startPositionMs: position);
|
||||
_roomPlayerVisible = wasRoomPlayerVisible;
|
||||
} catch (_) {
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
} finally {
|
||||
_isRoutingSwitching = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,11 +382,13 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_isPlaying = true;
|
||||
_isPaused = false;
|
||||
_startPositionTimer();
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
} catch (_) {
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_stopPositionTimer();
|
||||
_syncPublishingState();
|
||||
SCTts.show("Music playback failed");
|
||||
notifyListeners();
|
||||
} finally {
|
||||
@ -401,15 +444,20 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_isPlaying = true;
|
||||
_isPaused = false;
|
||||
_startPositionTimer();
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
break;
|
||||
case AudioMixingStateType.audioMixingStatePaused:
|
||||
_isPlaying = false;
|
||||
_isPaused = true;
|
||||
_stopPositionTimer();
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
break;
|
||||
case AudioMixingStateType.audioMixingStateStopped:
|
||||
if (_isRoutingSwitching) {
|
||||
return;
|
||||
}
|
||||
if (DateTime.now().millisecondsSinceEpoch < _ignoreStoppedUntilMs) {
|
||||
return;
|
||||
}
|
||||
@ -423,6 +471,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_positionMs = 0;
|
||||
_syncPublishingState();
|
||||
notifyListeners();
|
||||
}
|
||||
break;
|
||||
@ -430,6 +479,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_stopPositionTimer();
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_syncPublishingState();
|
||||
SCTts.show("Music playback failed");
|
||||
notifyListeners();
|
||||
break;
|
||||
@ -438,6 +488,45 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
|
||||
void _handleMicRouteChanged(bool isOnMic) {
|
||||
unawaited(syncPlaybackRouting());
|
||||
_syncPublishingState();
|
||||
}
|
||||
|
||||
void _syncPublishingState({bool force = false}) {
|
||||
final rtcProvider = _rtcProvider;
|
||||
if (rtcProvider == null) {
|
||||
_updatePublishingStateHeartbeat(false);
|
||||
return;
|
||||
}
|
||||
final publishing = isPublishingToRoom;
|
||||
_updatePublishingStateHeartbeat(publishing);
|
||||
if (!force && publishing == _lastPublishedRoomState) {
|
||||
return;
|
||||
}
|
||||
_lastPublishedRoomState = publishing;
|
||||
rtcProvider.publishCurrentUserRoomMusicState(publishing);
|
||||
}
|
||||
|
||||
void _updatePublishingStateHeartbeat(bool publishing) {
|
||||
if (!publishing) {
|
||||
_publishingStateHeartbeatTimer?.cancel();
|
||||
_publishingStateHeartbeatTimer = null;
|
||||
return;
|
||||
}
|
||||
if (_publishingStateHeartbeatTimer != null) {
|
||||
return;
|
||||
}
|
||||
_publishingStateHeartbeatTimer = Timer.periodic(
|
||||
_publishingStateHeartbeatInterval,
|
||||
(_) {
|
||||
if (!isPublishingToRoom) {
|
||||
_publishingStateHeartbeatTimer?.cancel();
|
||||
_publishingStateHeartbeatTimer = null;
|
||||
_syncPublishingState(force: true);
|
||||
return;
|
||||
}
|
||||
_rtcProvider?.publishCurrentUserRoomMusicState(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _restoreCurrentAfterPlaylistChange() {
|
||||
@ -453,6 +542,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
_current = null;
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_syncPublishingState(force: true);
|
||||
_roomPlayerVisible = false;
|
||||
_positionMs = 0;
|
||||
_durationMs = 0;
|
||||
@ -462,6 +552,7 @@ class RoomMusicManager extends ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
_positionTimer?.cancel();
|
||||
_publishingStateHeartbeatTimer?.cancel();
|
||||
_rtcProvider?.setRoomMusicMixingStateListener(null);
|
||||
_rtcProvider?.setRoomMusicMicRouteChangedListener(null);
|
||||
super.dispose();
|
||||
|
||||
@ -101,7 +101,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
_errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}';
|
||||
debugPrint(_errorMessage);
|
||||
}
|
||||
|
||||
var details = response.productDetails;
|
||||
@ -117,7 +116,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
SCTts.show("Failed to retrieve iOS products: $e");
|
||||
_errorMessage = 'Failed to get iOS goods: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
@ -178,7 +176,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
SCTts.show("iOS Purchase failed: $e");
|
||||
_errorMessage = 'iOS purchase failed: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
@ -200,7 +197,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
break;
|
||||
|
||||
case PurchaseStatus.pending:
|
||||
debugPrint('iOS支付处理中: ${purchase.productID}');
|
||||
SCTts.show('Payment is being processed, please wait...');
|
||||
break;
|
||||
case PurchaseStatus.restored:
|
||||
@ -246,10 +242,15 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
// 🔥 iOS关键:必须完成交易
|
||||
await iap.completePurchase(purchase);
|
||||
// 更新用户信息
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
SCTts.show('Purchase is successful!');
|
||||
debugPrint('iOS购买成功: ${purchase.productID}');
|
||||
} catch (e) {
|
||||
if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) {
|
||||
SCTts.show('The order already exists!');
|
||||
@ -259,7 +260,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
SCTts.show("ios verification fails $e");
|
||||
_errorMessage = 'iOS verification failed: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
// 即使验证失败,也要完成交易,否则会卡住
|
||||
if (purchase.pendingCompletePurchase) {
|
||||
await iap.completePurchase(purchase);
|
||||
@ -272,7 +272,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 交付商品(与安卓相同)
|
||||
Future<void> _deliverProduct(String productId) async {
|
||||
debugPrint('交付iOS商品: $productId');
|
||||
// 使用相同的业务逻辑
|
||||
}
|
||||
|
||||
@ -292,7 +291,6 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
break;
|
||||
}
|
||||
|
||||
debugPrint(_errorMessage);
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart
|
||||
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
|
||||
|
||||
class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
StreamSubscription<List<PurchaseDetails>>? _subscription;
|
||||
StreamSubscription<List<PurchaseDetails>>? _subscription;
|
||||
final InAppPurchase iap = InAppPurchase.instance;
|
||||
|
||||
// 状态管理变量
|
||||
@ -109,7 +109,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
);
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
_errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}';
|
||||
debugPrint(_errorMessage);
|
||||
}
|
||||
var dtails = response.productDetails;
|
||||
if (dtails.isNotEmpty) {
|
||||
@ -146,15 +145,10 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 打印商品信息用于调试
|
||||
for (var product in _products) {
|
||||
debugPrint(
|
||||
'商品: ${product.produc.id}, 类型: ${_productTypeMap[product.produc.id]}, 价格: ${product.produc.price}',
|
||||
);
|
||||
}
|
||||
for (var product in _products) {}
|
||||
} catch (e) {
|
||||
SCTts.show("Failed to retrieve the product: $e");
|
||||
_errorMessage = '获取商品失败: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
@ -217,7 +211,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
break;
|
||||
|
||||
case PurchaseStatus.pending:
|
||||
debugPrint('支付处理中: ${purchase.productID}');
|
||||
break;
|
||||
|
||||
case PurchaseStatus.restored:
|
||||
@ -235,18 +228,17 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 新增:处理已拥有商品的情况
|
||||
Future<void> _handleAlreadyOwnedItem(PurchaseDetails purchase) async {
|
||||
try {
|
||||
debugPrint('检测到已拥有商品: ${purchase.productID},尝试消耗');
|
||||
|
||||
// 如果是消耗型商品,尝试消耗
|
||||
if (_getProductType(purchase.productID) == 'consumable') {
|
||||
await consumePurchase(purchase);
|
||||
// 消耗后重新获取余额
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
SCTts.show('outstanding purchases have been reinstated');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('处理已拥有商品失败: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 验证支付凭证
|
||||
@ -261,7 +253,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 2. 检查是否已经处理过这个购买
|
||||
if (!purchase.pendingCompletePurchase) {
|
||||
debugPrint('购买已处理过: ${purchase.productID}');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -292,15 +283,19 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 8. 更新用户余额
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
|
||||
SCTts.show('purchase successful');
|
||||
debugPrint('购买成功: ${purchase.productID}');
|
||||
} catch (e) {
|
||||
SCTts.show("verification failed: $e");
|
||||
_errorMessage = '验证失败: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
@ -309,7 +304,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 交付商品
|
||||
Future<void> _deliverProduct(String productId) async {
|
||||
debugPrint('交付商品: $productId');
|
||||
// 实现您的业务逻辑
|
||||
// 例如:增加用户余额、解锁功能等
|
||||
}
|
||||
@ -322,11 +316,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 尝试消耗购买
|
||||
await consumePurchase(purchase);
|
||||
|
||||
debugPrint('商品已成功消耗: ${purchase.productID}');
|
||||
} catch (e) {
|
||||
debugPrint('消耗商品失败,可能已自动消耗: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void _handleError(Object error, StackTrace stackTrace) {
|
||||
@ -348,22 +338,18 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 处理特定错误代码
|
||||
switch (error.code) {
|
||||
case 'payment-invalid':
|
||||
debugPrint('支付无效: 商品ID可能配置错误');
|
||||
SCTts.show(
|
||||
'Payment configuration error, please contact customer service',
|
||||
);
|
||||
break;
|
||||
case 'item-already-owned':
|
||||
debugPrint('商品已拥有,尝试消耗商品');
|
||||
SCTts.show('Unfinished purchase detected, processing...');
|
||||
break;
|
||||
case 'user-cancelled':
|
||||
debugPrint('用户取消购买');
|
||||
SCTts.show('Purchase cancelled');
|
||||
break;
|
||||
case 'service-timeout':
|
||||
case 'service-unavailable':
|
||||
debugPrint('Google Play服务不可用');
|
||||
SCTts.show(
|
||||
'Google Play services are temporarily unavailable, please try again later',
|
||||
);
|
||||
@ -373,7 +359,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
break;
|
||||
}
|
||||
|
||||
debugPrint(_errorMessage);
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
}
|
||||
@ -390,7 +375,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
await Future.delayed(Duration(milliseconds: 1000));
|
||||
} catch (e) {
|
||||
_errorMessage = '恢复购买失败: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
SCTts.show("Failed to restore purchase: ${e.toString()}");
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
@ -405,11 +389,9 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||||
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
await androidAddition.consumePurchase(purchase);
|
||||
debugPrint('已消耗商品: ${purchase.productID}');
|
||||
}
|
||||
} catch (e) {
|
||||
_errorMessage = '消耗商品失败: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,7 +428,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
SCTts.show("Purchase failed: $e");
|
||||
_errorMessage = '购买失败: ${e.toString()}';
|
||||
debugPrint(_errorMessage);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
@ -461,22 +442,12 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 给一点时间处理恢复的购买
|
||||
await Future.delayed(Duration(milliseconds: 550));
|
||||
} catch (e) {
|
||||
debugPrint('检查未完成购买时出错: $e');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 添加调试方法
|
||||
void logCurrentPurchaseStatus() {
|
||||
debugPrint('=== 当前购买状态 ===');
|
||||
debugPrint('可用商品数量: ${_products.length}');
|
||||
debugPrint('未完成购买数量: ${_purchases.length}');
|
||||
|
||||
for (var purchase in _purchases) {
|
||||
debugPrint('商品: ${purchase.productID}, 状态: ${purchase.status}');
|
||||
debugPrint('待完成: ${purchase.pendingCompletePurchase}');
|
||||
}
|
||||
debugPrint('==================');
|
||||
for (var purchase in _purchases) {}
|
||||
}
|
||||
|
||||
// 释放资源
|
||||
|
||||
@ -9,12 +9,12 @@ import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart
|
||||
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart';
|
||||
|
||||
class SocialChatRoomManager extends ChangeNotifier {
|
||||
MyRoomRes? myRoom;
|
||||
SCRoomContributeLevelRes? roomContributeLevelRes;
|
||||
String? _roomContributeLevelRoomId;
|
||||
bool isMyRoomLoading = false;
|
||||
bool hasLoadedMyRoom = false;
|
||||
class SocialChatRoomManager extends ChangeNotifier {
|
||||
MyRoomRes? myRoom;
|
||||
SCRoomContributeLevelRes? roomContributeLevelRes;
|
||||
String? _roomContributeLevelRoomId;
|
||||
bool isMyRoomLoading = false;
|
||||
bool hasLoadedMyRoom = false;
|
||||
|
||||
Future<void> fetchMyRoomData() async {
|
||||
if (isMyRoomLoading) {
|
||||
@ -43,6 +43,9 @@ class SocialChatRoomManager extends ChangeNotifier {
|
||||
if ((roomInfo.roomCover ?? "").trim().isNotEmpty) {
|
||||
myRoom?.setRoomCover = roomInfo.roomCover!;
|
||||
}
|
||||
if ((roomInfo.roomBackground ?? "").trim().isNotEmpty) {
|
||||
myRoom?.setRoomBackground = roomInfo.roomBackground!;
|
||||
}
|
||||
if (roomInfo.roomName != null) {
|
||||
myRoom?.setRoomName = roomInfo.roomName!;
|
||||
}
|
||||
@ -58,7 +61,6 @@ class SocialChatRoomManager extends ChangeNotifier {
|
||||
// 差异化:添加自定义名称参数(暂未使用,为未来扩展预留)
|
||||
if (customName != null) {
|
||||
// TODO: 实现自定义房间名称
|
||||
debugPrint('创建房间使用自定义名称: $customName');
|
||||
}
|
||||
await SCAccountRepository().createRoom();
|
||||
myRoom = await SCAccountRepository().myProfile();
|
||||
@ -74,41 +76,41 @@ class SocialChatRoomManager extends ChangeNotifier {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
|
||||
void clearContributionLevelData({bool notify = true}) {
|
||||
_roomContributeLevelRoomId = null;
|
||||
if (roomContributeLevelRes == null) {
|
||||
return;
|
||||
}
|
||||
roomContributeLevelRes = null;
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void fetchContributionLevelData(String roomId, {bool forceRefresh = false}) {
|
||||
final resolvedRoomId = roomId.trim();
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
clearContributionLevelData();
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldClear =
|
||||
forceRefresh || _roomContributeLevelRoomId != resolvedRoomId;
|
||||
_roomContributeLevelRoomId = resolvedRoomId;
|
||||
if (shouldClear && roomContributeLevelRes != null) {
|
||||
roomContributeLevelRes = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SCChatRoomRepository()
|
||||
.roomContributionActivity(resolvedRoomId)
|
||||
.then((value) {
|
||||
if (_roomContributeLevelRoomId != resolvedRoomId) {
|
||||
return;
|
||||
}
|
||||
roomContributeLevelRes = value;
|
||||
notifyListeners();
|
||||
})
|
||||
.catchError((_) {});
|
||||
}
|
||||
}
|
||||
void clearContributionLevelData({bool notify = true}) {
|
||||
_roomContributeLevelRoomId = null;
|
||||
if (roomContributeLevelRes == null) {
|
||||
return;
|
||||
}
|
||||
roomContributeLevelRes = null;
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void fetchContributionLevelData(String roomId, {bool forceRefresh = false}) {
|
||||
final resolvedRoomId = roomId.trim();
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
clearContributionLevelData();
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldClear =
|
||||
forceRefresh || _roomContributeLevelRoomId != resolvedRoomId;
|
||||
_roomContributeLevelRoomId = resolvedRoomId;
|
||||
if (shouldClear && roomContributeLevelRes != null) {
|
||||
roomContributeLevelRes = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SCChatRoomRepository()
|
||||
.roomContributionActivity(resolvedRoomId)
|
||||
.then((value) {
|
||||
if (_roomContributeLevelRoomId != resolvedRoomId) {
|
||||
return;
|
||||
}
|
||||
roomContributeLevelRes = value;
|
||||
notifyListeners();
|
||||
})
|
||||
.catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ class RoomProfile {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -85,6 +86,7 @@ class RoomProfile {
|
||||
_roomAccount = roomAccount;
|
||||
_roomBadgeIcons = roomBadgeIcons;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomGameIcon = roomGameIcon;
|
||||
_roomName = roomName;
|
||||
@ -114,6 +116,7 @@ class RoomProfile {
|
||||
? json['roomBadgeIcons'].cast<String>()
|
||||
: [];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomGameIcon = json['roomGameIcon'];
|
||||
_roomName = json['roomName'];
|
||||
@ -146,6 +149,7 @@ class RoomProfile {
|
||||
String? _roomAccount;
|
||||
List<String>? _roomBadgeIcons;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomGameIcon;
|
||||
String? _roomName;
|
||||
@ -166,6 +170,7 @@ class RoomProfile {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -186,6 +191,7 @@ class RoomProfile {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||
roomName: roomName ?? _roomName,
|
||||
@ -206,6 +212,7 @@ class RoomProfile {
|
||||
String? get roomAccount => _roomAccount;
|
||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get roomGameIcon => _roomGameIcon;
|
||||
String? get roomName => _roomName;
|
||||
@ -243,6 +250,7 @@ class RoomProfile {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomGameIcon'] = _roomGameIcon;
|
||||
map['roomName'] = _roomName;
|
||||
|
||||
@ -1,182 +1,250 @@
|
||||
/// id : 0
|
||||
/// giftCode : ""
|
||||
/// giftPhoto : ""
|
||||
/// giftSourceUrl : ""
|
||||
/// giftCandy : 0.0
|
||||
/// giftIntegral : 0.0
|
||||
/// special : ""
|
||||
/// type : ""
|
||||
/// giftTab : ""
|
||||
/// standardId : 0
|
||||
/// explanationGift : false
|
||||
/// account : ""
|
||||
/// userId : 0
|
||||
/// giftName : ""
|
||||
/// expiredTime : 0
|
||||
|
||||
class SocialChatGiftRes {
|
||||
SocialChatGiftRes({
|
||||
String? id,
|
||||
String? giftCode,
|
||||
String? giftPhoto,
|
||||
String? giftSourceUrl,
|
||||
num? giftCandy,
|
||||
num? activityId,
|
||||
num? giftIntegral,
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
bool? explanationGift,
|
||||
String? account,
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
num? expiredTime,}){
|
||||
_id = id;
|
||||
_giftCode = giftCode;
|
||||
_giftPhoto = giftPhoto;
|
||||
_giftSourceUrl = giftSourceUrl;
|
||||
_giftCandy = giftCandy;
|
||||
_activityId = activityId;
|
||||
_giftIntegral = giftIntegral;
|
||||
_special = special;
|
||||
_type = type;
|
||||
_giftTab = giftTab;
|
||||
_standardId = standardId;
|
||||
_jumpUrl = jumpUrl;
|
||||
_bannerUrl = bannerUrl;
|
||||
_explanationGift = explanationGift;
|
||||
_account = account;
|
||||
_userId = userId;
|
||||
_giftName = giftName;
|
||||
_quantity = quantity;
|
||||
_expiredTime = expiredTime;
|
||||
}
|
||||
|
||||
SocialChatGiftRes.fromJson(dynamic json) {
|
||||
_id = json['id'];
|
||||
_giftCode = json['giftCode'];
|
||||
_giftPhoto = json['giftPhoto'];
|
||||
_giftSourceUrl = json['giftSourceUrl'];
|
||||
_giftCandy = json['giftCandy'];
|
||||
_activityId = json['activityId'];
|
||||
_giftIntegral = json['giftIntegral'];
|
||||
_special = json['special'];
|
||||
_type = json['type'];
|
||||
_giftTab = json['giftTab'];
|
||||
_standardId = json['standardId'];
|
||||
_jumpUrl = json['jumpUrl'];
|
||||
_bannerUrl = json['bannerUrl'];
|
||||
_explanationGift = json['explanationGift'];
|
||||
_account = json['account'];
|
||||
_userId = json['userId'];
|
||||
_giftName = json['giftName'];
|
||||
_quantity = json['quantity'];
|
||||
_expiredTime = json['expiredTime'];
|
||||
}
|
||||
String? _id;
|
||||
String? _giftCode;
|
||||
String? _giftPhoto;
|
||||
String? _giftSourceUrl;
|
||||
num? _giftCandy;
|
||||
num? _activityId;
|
||||
num? _giftIntegral;
|
||||
String? _special;
|
||||
String? _type;
|
||||
String? _giftTab;
|
||||
String? _standardId;
|
||||
String? _jumpUrl;
|
||||
String? _bannerUrl;
|
||||
bool? _explanationGift;
|
||||
String? _account;
|
||||
String? _userId;
|
||||
String? _giftName;
|
||||
String? _quantity;
|
||||
num? _expiredTime;
|
||||
SocialChatGiftRes copyWith({ String? id,
|
||||
String? giftCode,
|
||||
String? giftPhoto,
|
||||
String? giftSourceUrl,
|
||||
num? giftCandy,
|
||||
num? activityId,
|
||||
num? giftIntegral,
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
bool? explanationGift,
|
||||
String? account,
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
num? expiredTime,
|
||||
}) => SocialChatGiftRes( id: id ?? _id,
|
||||
giftCode: giftCode ?? _giftCode,
|
||||
giftPhoto: giftPhoto ?? _giftPhoto,
|
||||
giftSourceUrl: giftSourceUrl ?? _giftSourceUrl,
|
||||
giftCandy: giftCandy ?? _giftCandy,
|
||||
activityId: activityId ?? _activityId,
|
||||
giftIntegral: giftIntegral ?? _giftIntegral,
|
||||
special: special ?? _special,
|
||||
type: type ?? _type,
|
||||
giftTab: giftTab ?? _giftTab,
|
||||
standardId: standardId ?? _standardId,
|
||||
jumpUrl: jumpUrl ?? _jumpUrl,
|
||||
bannerUrl: bannerUrl ?? _bannerUrl,
|
||||
explanationGift: explanationGift ?? _explanationGift,
|
||||
account: account ?? _account,
|
||||
userId: userId ?? _userId,
|
||||
giftName: giftName ?? _giftName,
|
||||
quantity: quantity ?? _quantity,
|
||||
expiredTime: expiredTime ?? _expiredTime,
|
||||
);
|
||||
String? get id => _id;
|
||||
String? get giftCode => _giftCode;
|
||||
String? get giftPhoto => _giftPhoto;
|
||||
String? get giftSourceUrl => _giftSourceUrl;
|
||||
num? get giftCandy => _giftCandy;
|
||||
num? get activityId => _activityId;
|
||||
num? get giftIntegral => _giftIntegral;
|
||||
String? get special => _special;
|
||||
String? get type => _type;
|
||||
String? get giftTab => _giftTab;
|
||||
String? get standardId => _standardId;
|
||||
String? get jumpUrl => _jumpUrl;
|
||||
String? get bannerUrl => _bannerUrl;
|
||||
bool? get explanationGift => _explanationGift;
|
||||
String? get account => _account;
|
||||
String? get userId => _userId;
|
||||
String? get giftName => _giftName;
|
||||
String? get quantity => _quantity;
|
||||
num? get expiredTime => _expiredTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['giftCode'] = _giftCode;
|
||||
map['giftPhoto'] = _giftPhoto;
|
||||
map['giftSourceUrl'] = _giftSourceUrl;
|
||||
map['giftCandy'] = _giftCandy;
|
||||
map['activityId'] = _activityId;
|
||||
map['giftIntegral'] = _giftIntegral;
|
||||
map['special'] = _special;
|
||||
map['type'] = _type;
|
||||
map['giftTab'] = _giftTab;
|
||||
map['standardId'] = _standardId;
|
||||
map['jumpUrl'] = _jumpUrl;
|
||||
map['bannerUrl'] = _bannerUrl;
|
||||
map['explanationGift'] = _explanationGift;
|
||||
map['account'] = _account;
|
||||
map['userId'] = _userId;
|
||||
map['giftName'] = _giftName;
|
||||
map['quantity'] = _quantity;
|
||||
map['expiredTime'] = _expiredTime;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
// id : 0
|
||||
// giftCode : ""
|
||||
// giftPhoto : ""
|
||||
// giftSourceUrl : ""
|
||||
// giftCandy : 0.0
|
||||
// giftIntegral : 0.0
|
||||
// special : ""
|
||||
// type : ""
|
||||
// giftTab : ""
|
||||
// standardId : 0
|
||||
// explanationGift : false
|
||||
// account : ""
|
||||
// userId : 0
|
||||
// giftName : ""
|
||||
// expiredTime : 0
|
||||
|
||||
class SocialChatGiftRes {
|
||||
SocialChatGiftRes({
|
||||
String? id,
|
||||
String? giftCode,
|
||||
String? giftPhoto,
|
||||
String? giftSourceUrl,
|
||||
num? giftCandy,
|
||||
num? activityId,
|
||||
num? giftIntegral,
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
bool? explanationGift,
|
||||
String? account,
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
num? expiredTime,
|
||||
}) {
|
||||
_id = id;
|
||||
_giftCode = giftCode;
|
||||
_giftPhoto = giftPhoto;
|
||||
_giftSourceUrl = giftSourceUrl;
|
||||
_giftCandy = giftCandy;
|
||||
_activityId = activityId;
|
||||
_giftIntegral = giftIntegral;
|
||||
_special = special;
|
||||
_type = type;
|
||||
_giftTab = giftTab;
|
||||
_standardId = standardId;
|
||||
_jumpUrl = jumpUrl;
|
||||
_bannerUrl = bannerUrl;
|
||||
_explanationGift = explanationGift;
|
||||
_account = account;
|
||||
_userId = userId;
|
||||
_giftName = giftName;
|
||||
_quantity = quantity;
|
||||
_expiredTime = expiredTime;
|
||||
}
|
||||
|
||||
SocialChatGiftRes.fromJson(dynamic json) {
|
||||
final giftConfig =
|
||||
_giftResMap(json['giftConfig']) ??
|
||||
_giftResMap(json['gift']) ??
|
||||
_giftResMap(json['giftInfo']) ??
|
||||
_giftResMap(json['giftVO']);
|
||||
|
||||
_id = _giftResString(json['id'] ?? giftConfig?['id']);
|
||||
_giftCode = _giftResString(json['giftCode'] ?? giftConfig?['giftCode']);
|
||||
_giftPhoto = _giftResString(
|
||||
json['giftPhoto'] ??
|
||||
json['giftCover'] ??
|
||||
json['cover'] ??
|
||||
giftConfig?['giftPhoto'] ??
|
||||
giftConfig?['giftCover'] ??
|
||||
giftConfig?['cover'],
|
||||
);
|
||||
_giftSourceUrl = _giftResString(
|
||||
json['giftSourceUrl'] ??
|
||||
json['sourceUrl'] ??
|
||||
giftConfig?['giftSourceUrl'] ??
|
||||
giftConfig?['sourceUrl'],
|
||||
);
|
||||
_giftCandy = _giftResNum(
|
||||
json['giftCandy'] ??
|
||||
json['giftAmount'] ??
|
||||
json['giftValue'] ??
|
||||
json['amount'] ??
|
||||
json['price'] ??
|
||||
json['value'] ??
|
||||
json['coins'] ??
|
||||
giftConfig?['giftCandy'] ??
|
||||
giftConfig?['giftAmount'] ??
|
||||
giftConfig?['giftValue'] ??
|
||||
giftConfig?['amount'] ??
|
||||
giftConfig?['price'] ??
|
||||
giftConfig?['value'] ??
|
||||
giftConfig?['coins'],
|
||||
);
|
||||
_activityId = _giftResNum(json['activityId'] ?? giftConfig?['activityId']);
|
||||
_giftIntegral = _giftResNum(
|
||||
json['giftIntegral'] ??
|
||||
json['integral'] ??
|
||||
giftConfig?['giftIntegral'] ??
|
||||
giftConfig?['integral'],
|
||||
);
|
||||
_special = _giftResString(json['special'] ?? giftConfig?['special']);
|
||||
_type = _giftResString(json['type'] ?? giftConfig?['type']);
|
||||
_giftTab = _giftResString(json['giftTab'] ?? giftConfig?['giftTab']);
|
||||
_standardId = _giftResString(
|
||||
json['standardId'] ?? giftConfig?['standardId'],
|
||||
);
|
||||
_jumpUrl = _giftResString(json['jumpUrl'] ?? giftConfig?['jumpUrl']);
|
||||
_bannerUrl = _giftResString(json['bannerUrl'] ?? giftConfig?['bannerUrl']);
|
||||
_explanationGift = json['explanationGift'];
|
||||
_account = _giftResString(json['account'] ?? giftConfig?['account']);
|
||||
_userId = _giftResString(json['userId'] ?? giftConfig?['userId']);
|
||||
_giftName = _giftResString(
|
||||
json['giftName'] ??
|
||||
json['name'] ??
|
||||
giftConfig?['giftName'] ??
|
||||
giftConfig?['name'],
|
||||
);
|
||||
_quantity = _giftResString(
|
||||
json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'],
|
||||
);
|
||||
_expiredTime = _giftResNum(
|
||||
json['expiredTime'] ?? giftConfig?['expiredTime'],
|
||||
);
|
||||
}
|
||||
String? _id;
|
||||
String? _giftCode;
|
||||
String? _giftPhoto;
|
||||
String? _giftSourceUrl;
|
||||
num? _giftCandy;
|
||||
num? _activityId;
|
||||
num? _giftIntegral;
|
||||
String? _special;
|
||||
String? _type;
|
||||
String? _giftTab;
|
||||
String? _standardId;
|
||||
String? _jumpUrl;
|
||||
String? _bannerUrl;
|
||||
bool? _explanationGift;
|
||||
String? _account;
|
||||
String? _userId;
|
||||
String? _giftName;
|
||||
String? _quantity;
|
||||
num? _expiredTime;
|
||||
SocialChatGiftRes copyWith({
|
||||
String? id,
|
||||
String? giftCode,
|
||||
String? giftPhoto,
|
||||
String? giftSourceUrl,
|
||||
num? giftCandy,
|
||||
num? activityId,
|
||||
num? giftIntegral,
|
||||
String? special,
|
||||
String? type,
|
||||
String? giftTab,
|
||||
String? standardId,
|
||||
String? jumpUrl,
|
||||
String? bannerUrl,
|
||||
bool? explanationGift,
|
||||
String? account,
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
num? expiredTime,
|
||||
}) => SocialChatGiftRes(
|
||||
id: id ?? _id,
|
||||
giftCode: giftCode ?? _giftCode,
|
||||
giftPhoto: giftPhoto ?? _giftPhoto,
|
||||
giftSourceUrl: giftSourceUrl ?? _giftSourceUrl,
|
||||
giftCandy: giftCandy ?? _giftCandy,
|
||||
activityId: activityId ?? _activityId,
|
||||
giftIntegral: giftIntegral ?? _giftIntegral,
|
||||
special: special ?? _special,
|
||||
type: type ?? _type,
|
||||
giftTab: giftTab ?? _giftTab,
|
||||
standardId: standardId ?? _standardId,
|
||||
jumpUrl: jumpUrl ?? _jumpUrl,
|
||||
bannerUrl: bannerUrl ?? _bannerUrl,
|
||||
explanationGift: explanationGift ?? _explanationGift,
|
||||
account: account ?? _account,
|
||||
userId: userId ?? _userId,
|
||||
giftName: giftName ?? _giftName,
|
||||
quantity: quantity ?? _quantity,
|
||||
expiredTime: expiredTime ?? _expiredTime,
|
||||
);
|
||||
String? get id => _id;
|
||||
String? get giftCode => _giftCode;
|
||||
String? get giftPhoto => _giftPhoto;
|
||||
String? get giftSourceUrl => _giftSourceUrl;
|
||||
num? get giftCandy => _giftCandy;
|
||||
num? get activityId => _activityId;
|
||||
num? get giftIntegral => _giftIntegral;
|
||||
String? get special => _special;
|
||||
String? get type => _type;
|
||||
String? get giftTab => _giftTab;
|
||||
String? get standardId => _standardId;
|
||||
String? get jumpUrl => _jumpUrl;
|
||||
String? get bannerUrl => _bannerUrl;
|
||||
bool? get explanationGift => _explanationGift;
|
||||
String? get account => _account;
|
||||
String? get userId => _userId;
|
||||
String? get giftName => _giftName;
|
||||
String? get quantity => _quantity;
|
||||
num? get expiredTime => _expiredTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['giftCode'] = _giftCode;
|
||||
map['giftPhoto'] = _giftPhoto;
|
||||
map['giftSourceUrl'] = _giftSourceUrl;
|
||||
map['giftCandy'] = _giftCandy;
|
||||
map['activityId'] = _activityId;
|
||||
map['giftIntegral'] = _giftIntegral;
|
||||
map['special'] = _special;
|
||||
map['type'] = _type;
|
||||
map['giftTab'] = _giftTab;
|
||||
map['standardId'] = _standardId;
|
||||
map['jumpUrl'] = _jumpUrl;
|
||||
map['bannerUrl'] = _bannerUrl;
|
||||
map['explanationGift'] = _explanationGift;
|
||||
map['account'] = _account;
|
||||
map['userId'] = _userId;
|
||||
map['giftName'] = _giftName;
|
||||
map['quantity'] = _quantity;
|
||||
map['expiredTime'] = _expiredTime;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _giftResMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) return Map<String, dynamic>.from(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _giftResString(dynamic value) {
|
||||
if (value == null) return null;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
num? _giftResNum(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is num) return value;
|
||||
return num.tryParse(value.toString());
|
||||
}
|
||||
|
||||
@ -716,6 +716,7 @@ class RoomProfile2 {
|
||||
String? nationalFlag,
|
||||
String? roomAccount,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomName,
|
||||
String? sysOrigin,
|
||||
@ -733,6 +734,7 @@ class RoomProfile2 {
|
||||
_nationalFlag = nationalFlag;
|
||||
_roomAccount = roomAccount;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomName = roomName;
|
||||
_sysOrigin = sysOrigin;
|
||||
@ -752,6 +754,7 @@ class RoomProfile2 {
|
||||
_nationalFlag = json['nationalFlag'];
|
||||
_roomAccount = json['roomAccount'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomName = json['roomName'];
|
||||
_sysOrigin = json['sysOrigin'];
|
||||
@ -770,6 +773,7 @@ class RoomProfile2 {
|
||||
String? _nationalFlag;
|
||||
String? _roomAccount;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomName;
|
||||
String? _sysOrigin;
|
||||
@ -788,6 +792,7 @@ class RoomProfile2 {
|
||||
String? nationalFlag,
|
||||
String? roomAccount,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomName,
|
||||
String? sysOrigin,
|
||||
@ -805,6 +810,7 @@ class RoomProfile2 {
|
||||
nationalFlag: nationalFlag ?? _nationalFlag,
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomName: roomName ?? _roomName,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
@ -833,6 +839,7 @@ class RoomProfile2 {
|
||||
String? get roomAccount => _roomAccount;
|
||||
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
|
||||
String? get roomDesc => _roomDesc;
|
||||
|
||||
@ -857,6 +864,7 @@ class RoomProfile2 {
|
||||
map['nationalFlag'] = _nationalFlag;
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomName'] = _roomName;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
|
||||
@ -29,6 +29,7 @@ class MyRoomRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -49,6 +50,7 @@ class MyRoomRes {
|
||||
_roomAccount = roomAccount;
|
||||
_userId = userId;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomName = roomName;
|
||||
_roomDesc = roomDesc;
|
||||
_event = event;
|
||||
@ -71,6 +73,7 @@ class MyRoomRes {
|
||||
_roomAccount = json['roomAccount'];
|
||||
_userId = json['userId'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomName = json['roomName'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_event = json['event'];
|
||||
@ -98,6 +101,7 @@ class MyRoomRes {
|
||||
String? _roomAccount;
|
||||
String? _userId;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomName;
|
||||
String? _roomDesc;
|
||||
String? _event;
|
||||
@ -118,6 +122,7 @@ class MyRoomRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -138,6 +143,7 @@ class MyRoomRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
userId: userId ?? _userId,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomName: roomName ?? _roomName,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
event: event ?? _event,
|
||||
@ -158,6 +164,7 @@ class MyRoomRes {
|
||||
String? get roomAccount => _roomAccount;
|
||||
String? get userId => _userId;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomName => _roomName;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get event => _event;
|
||||
@ -174,6 +181,7 @@ class MyRoomRes {
|
||||
RoomCounter? get counter => _counter;
|
||||
List<UseBadges>? get useBadges => _useBadges;
|
||||
set setRoomCover(String value) => _roomCover = value;
|
||||
set setRoomBackground(String value) => _roomBackground = value;
|
||||
set setRoomName(String value) => _roomName = value;
|
||||
set setRoomDesc(String value) => _roomDesc = value;
|
||||
|
||||
@ -183,6 +191,7 @@ class MyRoomRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['userId'] = _userId;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomName'] = _roomName;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['event'] = _event;
|
||||
|
||||
@ -30,6 +30,7 @@ class SocialChatRoomRes {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -50,6 +51,7 @@ class SocialChatRoomRes {
|
||||
_roomAccount = roomAccount;
|
||||
_roomBadgeIcons = roomBadgeIcons;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomGameIcon = roomGameIcon;
|
||||
_roomName = roomName;
|
||||
@ -81,6 +83,7 @@ class SocialChatRoomRes {
|
||||
.toList()
|
||||
: null;
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomGameIcon = json['roomGameIcon'];
|
||||
_roomName = json['roomName'];
|
||||
@ -114,6 +117,7 @@ class SocialChatRoomRes {
|
||||
String? _roomAccount;
|
||||
List<String>? _roomBadgeIcons;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomGameIcon;
|
||||
String? _roomName;
|
||||
@ -135,6 +139,7 @@ class SocialChatRoomRes {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -155,6 +160,7 @@ class SocialChatRoomRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||
roomName: roomName ?? _roomName,
|
||||
@ -185,6 +191,7 @@ class SocialChatRoomRes {
|
||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
|
||||
String? get roomDesc => _roomDesc;
|
||||
|
||||
@ -228,6 +235,7 @@ class SocialChatRoomRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomGameIcon'] = _roomGameIcon;
|
||||
map['roomName'] = _roomName;
|
||||
|
||||
@ -16,6 +16,21 @@ class SCBroadCastLuckGiftPush {
|
||||
Data? get data => _data;
|
||||
String? get type => _type;
|
||||
|
||||
SCBroadCastLuckGiftPush mergeReward(SCBroadCastLuckGiftPush incoming) {
|
||||
final currentData = _data;
|
||||
final incomingData = incoming.data;
|
||||
if (currentData == null) {
|
||||
return incoming;
|
||||
}
|
||||
if (incomingData == null) {
|
||||
return this;
|
||||
}
|
||||
return SCBroadCastLuckGiftPush(
|
||||
data: currentData.mergeReward(incomingData),
|
||||
type: _type ?? incoming.type,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
if (_data != null) {
|
||||
@ -164,6 +179,35 @@ class Data {
|
||||
bool get isBigReward =>
|
||||
normalizedMultipleType == 'BIG_WIN' || normalizedMultipleType == 'SUPER';
|
||||
|
||||
Data mergeReward(Data incoming) {
|
||||
return Data(
|
||||
acceptNickname: _firstText(_acceptNickname, incoming.acceptNickname),
|
||||
acceptUserId: _firstText(_acceptUserId, incoming.acceptUserId),
|
||||
account: _firstText(_account, incoming.account),
|
||||
avatarFrameCover: _firstText(
|
||||
_avatarFrameCover,
|
||||
incoming.avatarFrameCover,
|
||||
),
|
||||
avatarFrameSvg: _firstText(_avatarFrameSvg, incoming.avatarFrameSvg),
|
||||
balance: incoming.balance ?? _balance,
|
||||
awardAmount: _sumNum(_awardAmount, incoming.awardAmount),
|
||||
giftCandy: _giftCandy ?? incoming.giftCandy,
|
||||
giftCover: _firstText(_giftCover, incoming.giftCover),
|
||||
giftQuantity: _sumNum(_giftQuantity, incoming.giftQuantity),
|
||||
globalNews: (_globalNews ?? false) || (incoming.globalNews ?? false),
|
||||
multiple: _maxNum(_multiple, incoming.multiple),
|
||||
multipleType: _strongerMultipleType(_multipleType, incoming.multipleType),
|
||||
nickname: _firstText(_nickname, incoming.nickname),
|
||||
regionCode: _firstText(_regionCode, incoming.regionCode),
|
||||
roomAccount: _firstText(_roomAccount, incoming.roomAccount),
|
||||
roomId: _firstText(_roomId, incoming.roomId),
|
||||
giftId: _firstText(_giftId, incoming.giftId),
|
||||
sendUserId: _firstText(_sendUserId, incoming.sendUserId),
|
||||
sysOrigin: _firstText(_sysOrigin, incoming.sysOrigin),
|
||||
userAvatar: _firstText(_userAvatar, incoming.userAvatar),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _asString(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@ -198,6 +242,57 @@ class Data {
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _firstText(String? current, String? incoming) {
|
||||
final currentText = current?.trim();
|
||||
if (currentText != null && currentText.isNotEmpty) {
|
||||
return current;
|
||||
}
|
||||
final incomingText = incoming?.trim();
|
||||
if (incomingText != null && incomingText.isNotEmpty) {
|
||||
return incoming;
|
||||
}
|
||||
return current ?? incoming;
|
||||
}
|
||||
|
||||
static num? _sumNum(num? current, num? incoming) {
|
||||
if (current == null) {
|
||||
return incoming;
|
||||
}
|
||||
if (incoming == null) {
|
||||
return current;
|
||||
}
|
||||
return current + incoming;
|
||||
}
|
||||
|
||||
static num? _maxNum(num? current, num? incoming) {
|
||||
if (current == null) {
|
||||
return incoming;
|
||||
}
|
||||
if (incoming == null) {
|
||||
return current;
|
||||
}
|
||||
return current >= incoming ? current : incoming;
|
||||
}
|
||||
|
||||
static String? _strongerMultipleType(String? current, String? incoming) {
|
||||
return _multipleTypeRank(incoming) > _multipleTypeRank(current)
|
||||
? incoming
|
||||
: current ?? incoming;
|
||||
}
|
||||
|
||||
static int _multipleTypeRank(String? value) {
|
||||
switch ((value ?? '').trim().toUpperCase()) {
|
||||
case 'SUPER':
|
||||
return 3;
|
||||
case 'BIG_WIN':
|
||||
return 2;
|
||||
case 'WIN':
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['acceptNickname'] = _acceptNickname;
|
||||
|
||||
@ -21,6 +21,7 @@ class SCEditRoomInfoRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -38,6 +39,7 @@ class SCEditRoomInfoRes {
|
||||
_roomAccount = roomAccount;
|
||||
_userId = userId;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomName = roomName;
|
||||
_roomDesc = roomDesc;
|
||||
_event = event;
|
||||
@ -57,6 +59,7 @@ class SCEditRoomInfoRes {
|
||||
_roomAccount = json['roomAccount'];
|
||||
_userId = json['userId'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomName = json['roomName'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_event = json['event'];
|
||||
@ -74,6 +77,7 @@ class SCEditRoomInfoRes {
|
||||
String? _roomAccount;
|
||||
String? _userId;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomName;
|
||||
String? _roomDesc;
|
||||
String? _event;
|
||||
@ -91,6 +95,7 @@ class SCEditRoomInfoRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -108,6 +113,7 @@ class SCEditRoomInfoRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
userId: userId ?? _userId,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomName: roomName ?? _roomName,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
event: event ?? _event,
|
||||
@ -125,6 +131,7 @@ class SCEditRoomInfoRes {
|
||||
String? get roomAccount => _roomAccount;
|
||||
String? get userId => _userId;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomName => _roomName;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get event => _event;
|
||||
@ -144,6 +151,7 @@ class SCEditRoomInfoRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['userId'] = _userId;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomName'] = _roomName;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['event'] = _event;
|
||||
|
||||
107
lib/shared/business_logic/models/res/sc_entry_popup_res.dart
Normal file
107
lib/shared/business_logic/models/res/sc_entry_popup_res.dart
Normal file
@ -0,0 +1,107 @@
|
||||
class SCEntryPopupRes {
|
||||
SCEntryPopupRes({
|
||||
required this.requestId,
|
||||
required this.queue,
|
||||
required this.popups,
|
||||
});
|
||||
|
||||
final String requestId;
|
||||
final List<String> queue;
|
||||
final Map<String, SCEntryPopupItem> popups;
|
||||
|
||||
factory SCEntryPopupRes.fromJson(dynamic json) {
|
||||
final map = json is Map<String, dynamic> ? json : const <String, dynamic>{};
|
||||
final queue =
|
||||
(map['queue'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.map((item) => item.toString().trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList();
|
||||
final popups = <String, SCEntryPopupItem>{};
|
||||
for (final entry in map.entries) {
|
||||
final key = entry.key;
|
||||
if (key == 'requestId' || key == 'queue') {
|
||||
continue;
|
||||
}
|
||||
final value = entry.value;
|
||||
if (value is Map<String, dynamic>) {
|
||||
popups[key] = SCEntryPopupItem.fromJson(key, value);
|
||||
}
|
||||
}
|
||||
return SCEntryPopupRes(
|
||||
requestId: map['requestId']?.toString() ?? '',
|
||||
queue: queue,
|
||||
popups: popups,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SCEntryPopupItem {
|
||||
SCEntryPopupItem({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.limit,
|
||||
required this.popupId,
|
||||
required this.version,
|
||||
required this.priority,
|
||||
required this.title,
|
||||
required this.image,
|
||||
required this.jumpType,
|
||||
required this.jumpUrl,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final bool value;
|
||||
final int limit;
|
||||
final String popupId;
|
||||
final int version;
|
||||
final int priority;
|
||||
final String title;
|
||||
final String image;
|
||||
final String jumpType;
|
||||
final String jumpUrl;
|
||||
final String reason;
|
||||
|
||||
factory SCEntryPopupItem.fromJson(String key, Map<String, dynamic> json) {
|
||||
return SCEntryPopupItem(
|
||||
key: key,
|
||||
value: _asBool(json['value']),
|
||||
limit: _asInt(json['limit']),
|
||||
popupId: json['popupId']?.toString() ?? '',
|
||||
version: _asInt(json['version']),
|
||||
priority: _asInt(json['priority']),
|
||||
title: json['title']?.toString() ?? '',
|
||||
image: json['image']?.toString() ?? '',
|
||||
jumpType: json['jumpType']?.toString() ?? '',
|
||||
jumpUrl: json['jumpUrl']?.toString() ?? '',
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _asBool(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return normalized == 'true' || normalized == '1';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
if (value is String) {
|
||||
return int.tryParse(value.trim()) ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
class SCRoomRedPacketConfigRes {
|
||||
const SCRoomRedPacketConfigRes({
|
||||
this.enabled = false,
|
||||
this.dailySendLimit = 0,
|
||||
this.todaySendCount = 0,
|
||||
this.delaySeconds = 0,
|
||||
this.refundAfterSeconds = 0,
|
||||
this.amountOptions = const <int>[],
|
||||
this.countOptions = const <int>[],
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketConfigRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketConfigRes(
|
||||
enabled: _boolValue(map['enabled']),
|
||||
dailySendLimit: _intValue(map['dailySendLimit']),
|
||||
todaySendCount: _intValue(map['todaySendCount']),
|
||||
delaySeconds: _intValue(map['delaySeconds']),
|
||||
refundAfterSeconds: _intValue(map['refundAfterSeconds']),
|
||||
amountOptions: _intListValue(map['amountOptions']),
|
||||
countOptions: _intListValue(map['countOptions']),
|
||||
);
|
||||
}
|
||||
|
||||
final bool enabled;
|
||||
final int dailySendLimit;
|
||||
final int todaySendCount;
|
||||
final int delaySeconds;
|
||||
final int refundAfterSeconds;
|
||||
final List<int> amountOptions;
|
||||
final List<int> countOptions;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'enabled': enabled,
|
||||
'dailySendLimit': dailySendLimit,
|
||||
'todaySendCount': todaySendCount,
|
||||
'delaySeconds': delaySeconds,
|
||||
'refundAfterSeconds': refundAfterSeconds,
|
||||
'amountOptions': amountOptions,
|
||||
'countOptions': countOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRedPacketImGroupRes {
|
||||
const SCRoomRedPacketImGroupRes({
|
||||
this.configured = false,
|
||||
this.sysOrigin = '',
|
||||
this.regionCode = '',
|
||||
this.groupId = '',
|
||||
this.groupName = '',
|
||||
this.status = '',
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketImGroupRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketImGroupRes(
|
||||
configured: _boolValue(map['configured']),
|
||||
sysOrigin: map['sysOrigin']?.toString() ?? '',
|
||||
regionCode: map['regionCode']?.toString() ?? '',
|
||||
groupId: map['groupId']?.toString() ?? '',
|
||||
groupName: map['groupName']?.toString() ?? '',
|
||||
status: map['status']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
final bool configured;
|
||||
final String sysOrigin;
|
||||
final String regionCode;
|
||||
final String groupId;
|
||||
final String groupName;
|
||||
final String status;
|
||||
|
||||
bool get canJoin => configured && groupId.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
class SCRoomRedPacketPresenceRes {
|
||||
const SCRoomRedPacketPresenceRes({
|
||||
this.userId = 0,
|
||||
this.sysOrigin = '',
|
||||
this.roomId = '',
|
||||
this.regionCode = '',
|
||||
this.ttlSeconds = 0,
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketPresenceRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketPresenceRes(
|
||||
userId: _intValue(map['userId']),
|
||||
sysOrigin: map['sysOrigin']?.toString() ?? '',
|
||||
roomId: map['roomId']?.toString() ?? '',
|
||||
regionCode: map['regionCode']?.toString() ?? '',
|
||||
ttlSeconds: _intValue(map['ttlSeconds']),
|
||||
);
|
||||
}
|
||||
|
||||
final int userId;
|
||||
final String sysOrigin;
|
||||
final String roomId;
|
||||
final String regionCode;
|
||||
final int ttlSeconds;
|
||||
}
|
||||
|
||||
int _intValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.round();
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value)?.round() ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _boolValue(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return normalized == 'true' || normalized == '1' || normalized == 'yes';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<int> _intListValue(dynamic value) {
|
||||
if (value is! List) {
|
||||
return const <int>[];
|
||||
}
|
||||
return value.map(_intValue).where((item) => item > 0).toList(growable: false);
|
||||
}
|
||||
@ -17,22 +17,23 @@
|
||||
|
||||
class SCRoomRedPacketDetailRes {
|
||||
SCRoomRedPacketDetailRes({
|
||||
String? packetId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? userAvatar,
|
||||
num? packetType,
|
||||
String? packetTypeDesc,
|
||||
num? totalAmount,
|
||||
num? totalCount,
|
||||
num? remainAmount,
|
||||
num? remainCount,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
num? myAmount,
|
||||
bool? grabbed,
|
||||
List<Records>? records,
|
||||
String? createTime,}){
|
||||
String? packetId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? userAvatar,
|
||||
num? packetType,
|
||||
String? packetTypeDesc,
|
||||
num? totalAmount,
|
||||
num? totalCount,
|
||||
num? remainAmount,
|
||||
num? remainCount,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
num? myAmount,
|
||||
bool? grabbed,
|
||||
List<Records>? records,
|
||||
String? createTime,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -49,23 +50,25 @@ class SCRoomRedPacketDetailRes {
|
||||
_grabbed = grabbed;
|
||||
_records = records;
|
||||
_createTime = createTime;
|
||||
}
|
||||
}
|
||||
|
||||
SCRoomRedPacketDetailRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_userId = json['userId'];
|
||||
_userName = json['userName'];
|
||||
_packetId = json['packetId']?.toString() ?? json['packetNo']?.toString();
|
||||
_userId = _numOrNull(json['userId']);
|
||||
_userName = json['userName'] ?? json['userNickname'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_packetType = json['packetType'];
|
||||
_packetType = _numOrNull(json['packetType']);
|
||||
_packetTypeDesc = json['packetTypeDesc'];
|
||||
_totalAmount = json['totalAmount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_remainAmount = json['remainAmount'];
|
||||
_remainCount = json['remainCount'];
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_myAmount = json['myAmount'];
|
||||
_totalAmount = _numOrNull(json['totalAmount']);
|
||||
_totalCount = _numOrNull(json['totalCount']);
|
||||
_remainAmount = _numOrNull(json['remainAmount']);
|
||||
_remainCount = _numOrNull(json['remainCount']);
|
||||
_status = _numOrNull(json['status']);
|
||||
_statusDesc = json['statusDesc']?.toString() ?? json['status']?.toString();
|
||||
_myAmount = _numOrNull(json['myAmount']);
|
||||
_grabbed = json['grabbed'];
|
||||
_expireTime = json['expireTime']?.toString();
|
||||
_claimStartTime = json['claimStartTime']?.toString();
|
||||
if (json['records'] != null) {
|
||||
_records = [];
|
||||
json['records'].forEach((v) {
|
||||
@ -90,6 +93,8 @@ class SCRoomRedPacketDetailRes {
|
||||
bool? _grabbed;
|
||||
List<Records>? _records;
|
||||
String? _createTime;
|
||||
String? _expireTime;
|
||||
String? _claimStartTime;
|
||||
|
||||
String? get packetId => _packetId;
|
||||
num? get userId => _userId;
|
||||
@ -107,6 +112,8 @@ class SCRoomRedPacketDetailRes {
|
||||
bool? get grabbed => _grabbed;
|
||||
List<Records>? get records => _records;
|
||||
String? get createTime => _createTime;
|
||||
String? get expireTime => _expireTime;
|
||||
String? get claimStartTime => _claimStartTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
@ -128,9 +135,10 @@ class SCRoomRedPacketDetailRes {
|
||||
map['records'] = _records?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
map['createTime'] = _createTime;
|
||||
map['expireTime'] = _expireTime;
|
||||
map['claimStartTime'] = _claimStartTime;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// recordId : ""
|
||||
@ -142,13 +150,14 @@ class SCRoomRedPacketDetailRes {
|
||||
|
||||
class Records {
|
||||
Records({
|
||||
String? recordId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? account,
|
||||
String? userAvatar,
|
||||
num? amount,
|
||||
String? grabTime,}){
|
||||
String? recordId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
String? account,
|
||||
String? userAvatar,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
}) {
|
||||
_recordId = recordId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -156,7 +165,7 @@ class Records {
|
||||
_userAvatar = userAvatar;
|
||||
_amount = amount;
|
||||
_grabTime = grabTime;
|
||||
}
|
||||
}
|
||||
|
||||
Records.fromJson(dynamic json) {
|
||||
_recordId = json['recordId'];
|
||||
@ -194,5 +203,14 @@ class Records {
|
||||
map['grabTime'] = _grabTime;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
num? _numOrNull(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -6,36 +6,42 @@
|
||||
|
||||
class SCRoomRedPacketGrabRes {
|
||||
SCRoomRedPacketGrabRes({
|
||||
String? packetId,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
num? remainCount,
|
||||
num? totalCount,}){
|
||||
String? packetId,
|
||||
num? amount,
|
||||
String? grabTime,
|
||||
num? remainCount,
|
||||
num? totalCount,
|
||||
String? status,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_amount = amount;
|
||||
_grabTime = grabTime;
|
||||
_remainCount = remainCount;
|
||||
_totalCount = totalCount;
|
||||
}
|
||||
_status = status;
|
||||
}
|
||||
|
||||
SCRoomRedPacketGrabRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_packetId = json['packetId'] ?? json['packetNo'];
|
||||
_amount = json['amount'];
|
||||
_grabTime = json['grabTime'];
|
||||
_grabTime = json['grabTime'] ?? json['claimTime'];
|
||||
_remainCount = json['remainCount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_status = json['status']?.toString();
|
||||
}
|
||||
String? _packetId;
|
||||
num? _amount;
|
||||
String? _grabTime;
|
||||
num? _remainCount;
|
||||
num? _totalCount;
|
||||
String? _status;
|
||||
|
||||
String? get packetId => _packetId;
|
||||
num? get amount => _amount;
|
||||
String? get grabTime => _grabTime;
|
||||
num? get remainCount => _remainCount;
|
||||
num? get totalCount => _totalCount;
|
||||
String? get status => _status;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
@ -44,7 +50,7 @@ class SCRoomRedPacketGrabRes {
|
||||
map['grabTime'] = _grabTime;
|
||||
map['remainCount'] = _remainCount;
|
||||
map['totalCount'] = _totalCount;
|
||||
map['status'] = _status;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
class SCRoomRedPacketListRes {
|
||||
SCRoomRedPacketListRes({
|
||||
String? packetId,
|
||||
String? packetMode,
|
||||
String? roomId,
|
||||
num? userId,
|
||||
String? userName,
|
||||
@ -34,6 +35,8 @@ class SCRoomRedPacketListRes {
|
||||
num? remainCount,
|
||||
num? expireMinutes,
|
||||
String? expireTime,
|
||||
String? claimStartTime,
|
||||
num? balanceAfter,
|
||||
int? countdown,
|
||||
num? status,
|
||||
String? statusDesc,
|
||||
@ -41,6 +44,7 @@ class SCRoomRedPacketListRes {
|
||||
String? createTime,
|
||||
}) {
|
||||
_packetId = packetId;
|
||||
_packetMode = packetMode;
|
||||
_roomId = roomId;
|
||||
_userId = userId;
|
||||
_userName = userName;
|
||||
@ -54,6 +58,8 @@ class SCRoomRedPacketListRes {
|
||||
_remainCount = remainCount;
|
||||
_expireMinutes = expireMinutes;
|
||||
_expireTime = expireTime;
|
||||
_claimStartTime = claimStartTime;
|
||||
_balanceAfter = balanceAfter;
|
||||
_countdown = countdown;
|
||||
_status = status;
|
||||
_statusDesc = statusDesc;
|
||||
@ -62,28 +68,33 @@ class SCRoomRedPacketListRes {
|
||||
}
|
||||
|
||||
SCRoomRedPacketListRes.fromJson(dynamic json) {
|
||||
_packetId = json['packetId'];
|
||||
_roomId = json['roomId'];
|
||||
_userId = json['userId'];
|
||||
_userName = json['userName'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
_packetType = json['packetType'];
|
||||
_packetTypeDesc = json['packetTypeDesc'];
|
||||
_sourceType = json['sourceType'];
|
||||
_totalAmount = json['totalAmount'];
|
||||
_totalCount = json['totalCount'];
|
||||
_remainAmount = json['remainAmount'];
|
||||
_remainCount = json['remainCount'];
|
||||
_expireMinutes = json['expireMinutes'];
|
||||
_expireTime = json['expireTime'];
|
||||
_countdown = json['countdown'];
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_packetId = json['packetId']?.toString() ?? json['packetNo']?.toString();
|
||||
_packetMode = json['packetMode']?.toString();
|
||||
_roomId = json['roomId']?.toString();
|
||||
_userId = _numOrNull(json['userId']);
|
||||
_userName =
|
||||
json['userName']?.toString() ?? json['userNickname']?.toString();
|
||||
_userAvatar = json['userAvatar']?.toString();
|
||||
_packetType = _numOrNull(json['packetType']);
|
||||
_packetTypeDesc = json['packetTypeDesc']?.toString();
|
||||
_sourceType = _numOrNull(json['sourceType']);
|
||||
_totalAmount = _numOrNull(json['totalAmount']);
|
||||
_totalCount = _numOrNull(json['totalCount']);
|
||||
_remainAmount = _numOrNull(json['remainAmount']);
|
||||
_remainCount = _numOrNull(json['remainCount']);
|
||||
_expireMinutes = _numOrNull(json['expireMinutes']);
|
||||
_expireTime = json['expireTime']?.toString();
|
||||
_claimStartTime = json['claimStartTime']?.toString();
|
||||
_balanceAfter = _numOrNull(json['balanceAfter']);
|
||||
_countdown = _intOrNull(json['countdown']);
|
||||
_status = _numOrNull(json['status']);
|
||||
_statusDesc = json['statusDesc']?.toString() ?? json['status']?.toString();
|
||||
_grabbed = json['grabbed'];
|
||||
_createTime = json['createTime'];
|
||||
}
|
||||
|
||||
String? _packetId;
|
||||
String? _packetMode;
|
||||
String? _roomId;
|
||||
num? _userId;
|
||||
String? _userName;
|
||||
@ -97,6 +108,8 @@ class SCRoomRedPacketListRes {
|
||||
num? _remainCount;
|
||||
num? _expireMinutes;
|
||||
String? _expireTime;
|
||||
String? _claimStartTime;
|
||||
num? _balanceAfter;
|
||||
int? _countdown;
|
||||
num? _status;
|
||||
String? _statusDesc;
|
||||
@ -105,6 +118,8 @@ class SCRoomRedPacketListRes {
|
||||
|
||||
String? get packetId => _packetId;
|
||||
|
||||
String? get packetMode => _packetMode;
|
||||
|
||||
String? get roomId => _roomId;
|
||||
|
||||
num? get userId => _userId;
|
||||
@ -131,6 +146,10 @@ class SCRoomRedPacketListRes {
|
||||
|
||||
String? get expireTime => _expireTime;
|
||||
|
||||
String? get claimStartTime => _claimStartTime;
|
||||
|
||||
num? get balanceAfter => _balanceAfter;
|
||||
|
||||
int? get countdown => _countdown;
|
||||
|
||||
num? get status => _status;
|
||||
@ -144,6 +163,7 @@ class SCRoomRedPacketListRes {
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['packetId'] = _packetId;
|
||||
map['packetMode'] = _packetMode;
|
||||
map['roomId'] = _roomId;
|
||||
map['userId'] = _userId;
|
||||
map['userName'] = _userName;
|
||||
@ -157,6 +177,8 @@ class SCRoomRedPacketListRes {
|
||||
map['remainCount'] = _remainCount;
|
||||
map['expireMinutes'] = _expireMinutes;
|
||||
map['expireTime'] = _expireTime;
|
||||
map['claimStartTime'] = _claimStartTime;
|
||||
map['balanceAfter'] = _balanceAfter;
|
||||
map['countdown'] = _countdown;
|
||||
map['status'] = _status;
|
||||
map['statusDesc'] = _statusDesc;
|
||||
@ -165,3 +187,17 @@ class SCRoomRedPacketListRes {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
num? _numOrNull(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _intOrNull(dynamic value) {
|
||||
return _numOrNull(value)?.round();
|
||||
}
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
class SCRoomRedPacketSentRecordRes {
|
||||
const SCRoomRedPacketSentRecordRes({
|
||||
this.packetNo = '',
|
||||
this.roomId = '',
|
||||
this.packetMode = '',
|
||||
this.totalAmount = 0,
|
||||
this.totalCount = 0,
|
||||
this.remainAmount = 0,
|
||||
this.remainCount = 0,
|
||||
this.refundAmount = 0,
|
||||
this.claimStartTime = '',
|
||||
this.expireTime = '',
|
||||
this.status = '',
|
||||
this.createTime = '',
|
||||
this.records = const <SCRoomRedPacketSentClaimRecordRes>[],
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketSentRecordRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketSentRecordRes(
|
||||
packetNo: _stringValue(map, const ['packetNo', 'packetId', 'id']),
|
||||
roomId: _stringValue(map, const ['roomId']),
|
||||
packetMode: _stringValue(map, const ['packetMode']),
|
||||
totalAmount: _numValue(map, const ['totalAmount', 'amount']),
|
||||
totalCount: _numValue(map, const ['totalCount', 'count']),
|
||||
remainAmount: _numValue(map, const ['remainAmount']),
|
||||
remainCount: _numValue(map, const ['remainCount']),
|
||||
refundAmount: _numValue(map, const [
|
||||
'refundAmount',
|
||||
'returnAmount',
|
||||
'returnedAmount',
|
||||
]),
|
||||
claimStartTime: _stringValue(map, const ['claimStartTime']),
|
||||
expireTime: _stringValue(map, const ['expireTime']),
|
||||
status: _stringValue(map, const ['status', 'statusDesc']),
|
||||
createTime: _stringValue(map, const ['createTime', 'sendTime', 'sentAt']),
|
||||
records: _recordListValue(
|
||||
map,
|
||||
).map(SCRoomRedPacketSentClaimRecordRes.fromJson).toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final String packetNo;
|
||||
final String roomId;
|
||||
final String packetMode;
|
||||
final num totalAmount;
|
||||
final num totalCount;
|
||||
final num remainAmount;
|
||||
final num remainCount;
|
||||
final num refundAmount;
|
||||
final String claimStartTime;
|
||||
final String expireTime;
|
||||
final String status;
|
||||
final String createTime;
|
||||
final List<SCRoomRedPacketSentClaimRecordRes> records;
|
||||
}
|
||||
|
||||
class SCRoomRedPacketSentClaimRecordRes {
|
||||
const SCRoomRedPacketSentClaimRecordRes({
|
||||
this.userName = '',
|
||||
this.userAvatar = '',
|
||||
this.amount = 0,
|
||||
this.claimTime = '',
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketSentClaimRecordRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketSentClaimRecordRes(
|
||||
userName: _stringValue(map, const [
|
||||
'userName',
|
||||
'userNickname',
|
||||
'nickname',
|
||||
'name',
|
||||
'account',
|
||||
]),
|
||||
userAvatar: _stringValue(map, const [
|
||||
'userAvatar',
|
||||
'avatar',
|
||||
'avatarUrl',
|
||||
]),
|
||||
amount: _numValue(map, const ['amount', 'claimAmount', 'grabAmount']),
|
||||
claimTime: _stringValue(map, const [
|
||||
'grabTime',
|
||||
'claimTime',
|
||||
'createTime',
|
||||
'time',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
final String userName;
|
||||
final String userAvatar;
|
||||
final num amount;
|
||||
final String claimTime;
|
||||
}
|
||||
|
||||
class SCRoomRedPacketClaimRecordsRes {
|
||||
const SCRoomRedPacketClaimRecordsRes({
|
||||
this.packetNo = '',
|
||||
this.totalCount = 0,
|
||||
this.claimedCount = 0,
|
||||
this.claimedAmount = 0,
|
||||
this.records = const <SCRoomRedPacketSentClaimRecordRes>[],
|
||||
});
|
||||
|
||||
factory SCRoomRedPacketClaimRecordsRes.fromJson(dynamic json) {
|
||||
final map =
|
||||
json is Map
|
||||
? json.map((key, value) => MapEntry(key.toString(), value))
|
||||
: const <String, dynamic>{};
|
||||
return SCRoomRedPacketClaimRecordsRes(
|
||||
packetNo: _stringValue(map, const ['packetNo', 'packetId', 'id']),
|
||||
totalCount: _numValue(map, const ['totalCount', 'count']),
|
||||
claimedCount: _numValue(map, const ['claimedCount', 'grabbedCount']),
|
||||
claimedAmount: _numValue(map, const ['claimedAmount', 'grabbedAmount']),
|
||||
records: _recordListValue(
|
||||
map,
|
||||
).map(SCRoomRedPacketSentClaimRecordRes.fromJson).toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final String packetNo;
|
||||
final num totalCount;
|
||||
final num claimedCount;
|
||||
final num claimedAmount;
|
||||
final List<SCRoomRedPacketSentClaimRecordRes> records;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'packetNo': packetNo,
|
||||
'totalCount': totalCount,
|
||||
'claimedCount': claimedCount,
|
||||
'claimedAmount': claimedAmount,
|
||||
'records': records
|
||||
.map(
|
||||
(record) => {
|
||||
'userName': record.userName,
|
||||
'userAvatar': record.userAvatar,
|
||||
'amount': record.amount,
|
||||
'claimTime': record.claimTime,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
String _stringValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final text = json[key]?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
num _numValue(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
final parsed = num.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<dynamic> _recordListValue(Map<String, dynamic> json) {
|
||||
for (final key in const [
|
||||
'records',
|
||||
'claimRecords',
|
||||
'grabRecords',
|
||||
'receiveRecords',
|
||||
'items',
|
||||
]) {
|
||||
final value = json[key];
|
||||
if (value is List) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return const <dynamic>[];
|
||||
}
|
||||
@ -1,15 +1,19 @@
|
||||
/// taskType : 0
|
||||
/// taskName : ""
|
||||
/// taskDesc : ""
|
||||
/// jumpPage : ""
|
||||
/// conditionType : ""
|
||||
/// sortOrder : 0
|
||||
/// taskId : 0
|
||||
/// taskStatus : 0
|
||||
/// cover : ""
|
||||
/// taskIcon : ""
|
||||
/// isRewardCollected : 0
|
||||
/// quantity : 0
|
||||
import 'dart:convert';
|
||||
|
||||
// taskType : 0
|
||||
// taskName : ""
|
||||
// taskDesc : ""
|
||||
// jumpPage : ""
|
||||
// jumpType : ""
|
||||
// roomId : ""
|
||||
// conditionType : ""
|
||||
// sortOrder : 0
|
||||
// taskId : "0"
|
||||
// taskStatus : 0
|
||||
// cover : ""
|
||||
// taskIcon : ""
|
||||
// isRewardCollected : 0
|
||||
// quantity : 0
|
||||
|
||||
class SCTaskListRes {
|
||||
SCTaskListRes({
|
||||
@ -17,14 +21,16 @@ class SCTaskListRes {
|
||||
String? taskName,
|
||||
String? taskDesc,
|
||||
String? jumpPage,
|
||||
String? jumpType,
|
||||
String? roomId,
|
||||
String? conditionType,
|
||||
num? sortOrder,
|
||||
num? taskId,
|
||||
String? taskId,
|
||||
num? taskStatus,
|
||||
String? cover,
|
||||
String? taskIcon,
|
||||
String? conditionValue,
|
||||
String? completedValue,
|
||||
num? completedValue,
|
||||
num? isRewardCollected,
|
||||
num? targetValue,
|
||||
num? quantity,
|
||||
@ -33,6 +39,8 @@ class SCTaskListRes {
|
||||
_taskName = taskName;
|
||||
_taskDesc = taskDesc;
|
||||
_jumpPage = jumpPage;
|
||||
_jumpType = jumpType;
|
||||
_roomId = roomId;
|
||||
_conditionType = conditionType;
|
||||
_sortOrder = sortOrder;
|
||||
_taskId = taskId;
|
||||
@ -47,38 +55,126 @@ class SCTaskListRes {
|
||||
}
|
||||
|
||||
SCTaskListRes.fromJson(dynamic json) {
|
||||
_taskType = json['taskType'];
|
||||
_taskName = json['taskName'];
|
||||
_taskDesc = json['taskDesc'];
|
||||
_jumpPage = json['jumpPage'];
|
||||
_conditionType = json['conditionType'];
|
||||
_sortOrder = json['sortOrder'];
|
||||
_taskId = json['taskId'];
|
||||
_taskStatus = json['taskStatus'];
|
||||
_cover = json['cover'];
|
||||
_taskIcon = json['taskIcon'];
|
||||
_conditionValue = json['conditionValue'];
|
||||
_completedValue = json['completedValue'];
|
||||
_isRewardCollected = json['isRewardCollected'];
|
||||
_quantity = json['quantity'];
|
||||
_targetValue = json['targetValue'];
|
||||
final map = _asMap(json) ?? const <String, dynamic>{};
|
||||
_rawJson = Map<String, dynamic>.from(map);
|
||||
_taskType = _asNum(map['taskType']);
|
||||
_taskName = _asString(map['taskName']);
|
||||
_taskDesc = _asString(map['taskDesc']);
|
||||
_jumpPage = _firstString([
|
||||
map['jumpPage'],
|
||||
map['jumpUrl'],
|
||||
map['jumpURI'],
|
||||
map['jumpUri'],
|
||||
map['route'],
|
||||
map['actionUrl'],
|
||||
map['targetUrl'],
|
||||
_nestedValue(map, 'jumpPage'),
|
||||
_nestedValue(map, 'jumpUrl'),
|
||||
_nestedValue(map, 'route'),
|
||||
_nestedValue(map, 'targetUrl'),
|
||||
]);
|
||||
_jumpType = _firstString([
|
||||
map['jumpType'],
|
||||
map['actionType'],
|
||||
map['clickType'],
|
||||
map['targetType'],
|
||||
_nestedValue(map, 'jumpType'),
|
||||
_nestedValue(map, 'actionType'),
|
||||
_nestedValue(map, 'targetType'),
|
||||
]);
|
||||
_roomId = _firstString([
|
||||
map['roomId'],
|
||||
map['targetRoomId'],
|
||||
map['jumpRoomId'],
|
||||
_nestedValue(map, 'roomId'),
|
||||
_nestedValue(map, 'targetRoomId'),
|
||||
_nestedValue(map, 'jumpRoomId'),
|
||||
]);
|
||||
_conditionType = _firstString([
|
||||
map['conditionType'],
|
||||
map['conditionCode'],
|
||||
_nestedValue(map, 'conditionType'),
|
||||
_nestedValue(map, 'conditionCode'),
|
||||
]);
|
||||
_sortOrder = _asNum(map['sortOrder']);
|
||||
_taskId = _asString(map['taskId']);
|
||||
_taskStatus = _asNum(map['taskStatus']);
|
||||
_cover = _asString(map['cover']);
|
||||
_taskIcon = _asString(map['taskIcon']);
|
||||
_conditionValue =
|
||||
_asString(map['conditionValue']) ??
|
||||
_asString(_nestedValue(map, 'conditionValue'));
|
||||
_completedValue = _firstNum([
|
||||
map['completedValue'],
|
||||
map['currentValue'],
|
||||
map['currentProgress'],
|
||||
map['progressValue'],
|
||||
map['finishValue'],
|
||||
map['finishedValue'],
|
||||
map['completedCount'],
|
||||
map['completeCount'],
|
||||
map['finishCount'],
|
||||
map['finishedCount'],
|
||||
map['doneCount'],
|
||||
map['current'],
|
||||
map['currentNum'],
|
||||
map['currentAmount'],
|
||||
_nestedValue(map, 'completedValue'),
|
||||
_nestedValue(map, 'currentValue'),
|
||||
_nestedValue(map, 'current'),
|
||||
_nestedValue(map, 'progressValue'),
|
||||
_nestedValue(map, 'completedCount'),
|
||||
_nestedValue(map, 'currentNum'),
|
||||
_nestedValue(map, 'currentAmount'),
|
||||
]);
|
||||
_isRewardCollected = _asNum(map['isRewardCollected']);
|
||||
_quantity = _asNum(map['quantity']);
|
||||
_targetValue = _firstNum([
|
||||
map['targetValue'],
|
||||
map['target'],
|
||||
map['goalValue'],
|
||||
map['requiredValue'],
|
||||
map['conditionValue'],
|
||||
map['conditionNum'],
|
||||
map['targetCount'],
|
||||
map['requiredCount'],
|
||||
map['targetNum'],
|
||||
map['requiredNum'],
|
||||
map['total'],
|
||||
_nestedValue(map, 'targetValue'),
|
||||
_nestedValue(map, 'target'),
|
||||
_nestedValue(map, 'goalValue'),
|
||||
_nestedValue(map, 'requiredValue'),
|
||||
_nestedValue(map, 'conditionValue'),
|
||||
_nestedValue(map, 'targetNum'),
|
||||
_nestedValue(map, 'requiredNum'),
|
||||
_nestedValue(map, 'total'),
|
||||
]);
|
||||
if (_completedValue == null &&
|
||||
_targetValue != null &&
|
||||
(_taskStatus ?? 0) == 1) {
|
||||
_completedValue = _targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
num? _taskType;
|
||||
String? _taskName;
|
||||
String? _taskDesc;
|
||||
String? _jumpPage;
|
||||
String? _jumpType;
|
||||
String? _roomId;
|
||||
String? _conditionType;
|
||||
num? _sortOrder;
|
||||
num? _taskId;
|
||||
String? _taskId;
|
||||
num? _taskStatus;
|
||||
String? _cover;
|
||||
String? _taskIcon;
|
||||
String? _conditionValue;
|
||||
String? _completedValue;
|
||||
num? _completedValue;
|
||||
num? _isRewardCollected;
|
||||
num? _quantity;
|
||||
num? _targetValue;
|
||||
Map<String, dynamic> _rawJson = const <String, dynamic>{};
|
||||
|
||||
num? get taskType => _taskType;
|
||||
|
||||
@ -88,11 +184,15 @@ class SCTaskListRes {
|
||||
|
||||
String? get jumpPage => _jumpPage;
|
||||
|
||||
String? get jumpType => _jumpType;
|
||||
|
||||
String? get roomId => _roomId;
|
||||
|
||||
String? get conditionType => _conditionType;
|
||||
|
||||
num? get sortOrder => _sortOrder;
|
||||
|
||||
num? get taskId => _taskId;
|
||||
String? get taskId => _taskId;
|
||||
|
||||
num? get taskStatus => _taskStatus;
|
||||
|
||||
@ -102,19 +202,24 @@ class SCTaskListRes {
|
||||
|
||||
String? get conditionValue => _conditionValue;
|
||||
|
||||
String? get completedValue => _completedValue;
|
||||
num? get completedValue => _completedValue;
|
||||
|
||||
num? get isRewardCollected => _isRewardCollected;
|
||||
|
||||
num? get quantity => _quantity;
|
||||
num? get targetValue => _targetValue;
|
||||
|
||||
Map<String, dynamic> get rawJson =>
|
||||
Map<String, dynamic>.unmodifiable(_rawJson);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['taskType'] = _taskType;
|
||||
map['taskName'] = _taskName;
|
||||
map['taskDesc'] = _taskDesc;
|
||||
map['jumpPage'] = _jumpPage;
|
||||
map['jumpType'] = _jumpType;
|
||||
map['roomId'] = _roomId;
|
||||
map['conditionType'] = _conditionType;
|
||||
map['sortOrder'] = _sortOrder;
|
||||
map['taskId'] = _taskId;
|
||||
@ -128,4 +233,80 @@ class SCTaskListRes {
|
||||
map['targetValue'] = _targetValue;
|
||||
return map;
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _asMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) return Map<String, dynamic>.from(value);
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(value);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
if (decoded is Map) return Map<String, dynamic>.from(decoded);
|
||||
} catch (_) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _asString(dynamic value) {
|
||||
if (value == null) return null;
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
static String? _firstString(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final text = _asString(value)?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static dynamic _nestedValue(Map<String, dynamic> map, String key) {
|
||||
const nestedKeys = [
|
||||
'jumpParams',
|
||||
'jumpParam',
|
||||
'jumpParameters',
|
||||
'params',
|
||||
'extra',
|
||||
'ext',
|
||||
'extend',
|
||||
'progress',
|
||||
'taskProgress',
|
||||
'condition',
|
||||
'taskCondition',
|
||||
'completion',
|
||||
'completionInfo',
|
||||
'progressInfo',
|
||||
'stat',
|
||||
'stats',
|
||||
];
|
||||
for (final nestedKey in nestedKeys) {
|
||||
final nested = _asMap(map[nestedKey]);
|
||||
if (nested != null && nested.containsKey(key)) {
|
||||
return nested[key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static num? _asNum(dynamic value) {
|
||||
if (value is num) return value;
|
||||
if (value is String) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) return null;
|
||||
return num.tryParse(text) ?? num.tryParse(text.replaceAll(',', ''));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static num? _firstNum(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final number = _asNum(value);
|
||||
if (number != null) {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
|
||||
@ -39,6 +40,9 @@ abstract class SocialChatConfigRepository {
|
||||
///获取平台banner.
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>? types});
|
||||
|
||||
///APP进场弹窗.
|
||||
Future<SCEntryPopupRes> entryPopup();
|
||||
|
||||
///获取商品配置
|
||||
Future<List<SCProductConfigRes>> productConfig();
|
||||
|
||||
|
||||
@ -16,8 +16,10 @@ import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_join_black_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_record_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart';
|
||||
@ -217,8 +219,9 @@ abstract class SocialChatRoomRepository {
|
||||
String roomId,
|
||||
String totalAmount,
|
||||
String totalCount,
|
||||
String expireMinutes,
|
||||
);
|
||||
String expireMinutes, {
|
||||
String packetMode = 'DELAYED',
|
||||
});
|
||||
|
||||
///抢红包
|
||||
Future<SCRoomRedPacketGrabRes> roomRedPacketGrab(String packetId);
|
||||
@ -226,6 +229,31 @@ abstract class SocialChatRoomRepository {
|
||||
///查询红包详情
|
||||
Future<SCRoomRedPacketDetailRes> roomRedPacketDetail(String packetId);
|
||||
|
||||
///查询语音房红包配置
|
||||
Future<SCRoomRedPacketConfigRes> roomRedPacketConfig();
|
||||
|
||||
///查询当前用户语区红包 IM 广播群
|
||||
Future<SCRoomRedPacketImGroupRes> roomRedPacketImGroup();
|
||||
|
||||
///上报房间红包在线态
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceHeartbeat(
|
||||
String roomId,
|
||||
);
|
||||
|
||||
///离开房间红包在线态
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceLeave(String roomId);
|
||||
|
||||
///查询红包发送记录
|
||||
Future<List<SCRoomRedPacketSentRecordRes>> roomRedPacketSentRecords({
|
||||
int cursor = 1,
|
||||
int limit = 20,
|
||||
});
|
||||
|
||||
///查询红包领取记录
|
||||
Future<SCRoomRedPacketClaimRecordsRes> roomRedPacketClaimRecords(
|
||||
String packetNo,
|
||||
);
|
||||
|
||||
///查询房间奖励信息
|
||||
Future<SCRoomRewardInfoRes> roomRewardInfo(String roomId);
|
||||
|
||||
|
||||
@ -90,6 +90,12 @@ abstract class SocialChatUserRepository {
|
||||
String event,
|
||||
);
|
||||
|
||||
///修改房间背景图
|
||||
Future<SCEditRoomInfoRes> updateRoomBackground(
|
||||
String roomId,
|
||||
String roomBackground,
|
||||
);
|
||||
|
||||
///获取指定用户信息
|
||||
Future<SocialChatUserProfile> loadUserInfo(String userId);
|
||||
|
||||
@ -210,7 +216,10 @@ abstract class SocialChatUserRepository {
|
||||
Future<List<SCTaskListRes>> tasks();
|
||||
|
||||
///领取奖励
|
||||
Future<bool> taskReward(num taskId);
|
||||
Future<bool> taskReward(String taskId);
|
||||
|
||||
///可领取任务数量
|
||||
Future<int> taskClaimableCount();
|
||||
|
||||
///我的访客列表
|
||||
Future<List<FollowUserRes>> visitorList(num pageNumber);
|
||||
@ -236,6 +245,12 @@ abstract class SocialChatUserRepository {
|
||||
///赠送道具券
|
||||
Future<bool> couponSend(String couponNo, String receiverId);
|
||||
|
||||
///查询已拥有徽章
|
||||
Future<List<WearBadge>> badgeOwnList(String type);
|
||||
|
||||
///切换佩戴徽章
|
||||
Future<bool> badgeToggle(String badgeId);
|
||||
|
||||
///处理用户违规
|
||||
Future<SCViolationHandleRes> userViolationHandle(
|
||||
String userId,
|
||||
@ -277,4 +292,21 @@ abstract class SocialChatUserRepository {
|
||||
|
||||
///注销账号
|
||||
Future<bool> logoutAccount();
|
||||
|
||||
///申请 CP 关系
|
||||
Future<bool> cpRelationshipSendApply(String acceptApplyUserId);
|
||||
|
||||
///处理 CP 申请
|
||||
Future<bool> cpRelationshipProcessApply(String applyId, bool agree);
|
||||
|
||||
///解除 CP 申请/关系
|
||||
Future<bool> cpRelationshipDismissApply(String cpUserId);
|
||||
|
||||
///更新 VIP 能力配置
|
||||
Future<bool> userVipAbilityUpdate({
|
||||
String? contactMode,
|
||||
bool? kickPrevention,
|
||||
bool? mysteriousInvisibility,
|
||||
bool? antiBlock,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
class SCFloatingMessage {
|
||||
static const String broadcastScopeRegion = 'region';
|
||||
|
||||
String? userId; // 可选:用户ID
|
||||
String? roomId; // 可选:房间ID
|
||||
String? toUserId; // 可选:用户ID
|
||||
@ -14,6 +16,7 @@ class SCFloatingMessage {
|
||||
num? coins;
|
||||
num? number;
|
||||
num? multiple;
|
||||
String? broadcastScope; // region:来自语区 IM 群,只能按区域飘屏规则展示
|
||||
int priority = 10; //排序权重
|
||||
|
||||
SCFloatingMessage({
|
||||
@ -33,6 +36,7 @@ class SCFloatingMessage {
|
||||
this.coins = 0,
|
||||
this.priority = 10,
|
||||
this.multiple = 10,
|
||||
this.broadcastScope = '',
|
||||
});
|
||||
|
||||
SCFloatingMessage.fromJson(dynamic json) {
|
||||
@ -52,8 +56,11 @@ class SCFloatingMessage {
|
||||
number = json['number'];
|
||||
priority = json['priority'];
|
||||
multiple = json['multiple'];
|
||||
broadcastScope = json['broadcastScope'];
|
||||
}
|
||||
|
||||
bool get isRegionBroadcast => broadcastScope == broadcastScopeRegion;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['userId'] = userId;
|
||||
@ -72,6 +79,7 @@ class SCFloatingMessage {
|
||||
map['number'] = number;
|
||||
map['priority'] = priority;
|
||||
map['multiple'] = multiple;
|
||||
map['broadcastScope'] = broadcastScope;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@ -10,6 +9,8 @@ class DataPersistence {
|
||||
"await_register_reward_socket";
|
||||
static const String _pendingRegisterRewardDialogKey =
|
||||
"pending_register_reward_dialog";
|
||||
static const String _pendingFirstRegisterRoomGameEventKey =
|
||||
"pending_first_register_room_game_event";
|
||||
static const String _pendingChannelAuthTypeKey = "pending_channel_auth_type";
|
||||
static const String _pendingChannelAuthOpenIdKey =
|
||||
"pending_channel_auth_open_id";
|
||||
@ -31,11 +32,9 @@ class DataPersistence {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_isInitialized = true;
|
||||
_initializationCompleter!.complete();
|
||||
debugPrint('SharedPreferences initialized successfully');
|
||||
} catch (e) {
|
||||
_initializationCompleter!.completeError(e);
|
||||
_initializationCompleter = null;
|
||||
debugPrint('SharedPreferences initialization failed: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -218,6 +217,18 @@ class DataPersistence {
|
||||
return await remove(_awaitRegisterRewardSocketKey);
|
||||
}
|
||||
|
||||
static Future<bool> setPendingFirstRegisterRoomGameEvent(bool value) async {
|
||||
return await setBool(_pendingFirstRegisterRoomGameEventKey, value);
|
||||
}
|
||||
|
||||
static bool getPendingFirstRegisterRoomGameEvent() {
|
||||
return getBool(_pendingFirstRegisterRoomGameEventKey);
|
||||
}
|
||||
|
||||
static Future<bool> clearPendingFirstRegisterRoomGameEvent() async {
|
||||
return await remove(_pendingFirstRegisterRoomGameEventKey);
|
||||
}
|
||||
|
||||
static Future<void> setPendingChannelAuth(
|
||||
String authType,
|
||||
String openId,
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
typedef FileCacheManager = MediaCache;
|
||||
|
||||
class MediaCache {
|
||||
late CacheManager _cacheManager;
|
||||
static FileCacheManager? _instance;
|
||||
@ -55,10 +56,8 @@ class MediaCache {
|
||||
await Directory("$temporaryPath/$musicPath").create(recursive: true);
|
||||
|
||||
soundPath = "$temporaryPath/sound";
|
||||
print('语音文件夹路径: $soundPath');
|
||||
return temporaryPath;
|
||||
} catch (e) {
|
||||
print('目录创建失败: $e');
|
||||
rethrow; // 或者返回一个备用路径
|
||||
}
|
||||
}
|
||||
@ -66,10 +65,7 @@ class MediaCache {
|
||||
/// 下载或获取缓存文件
|
||||
/// [url] 文件网络地址
|
||||
/// [ignoreCache] 是否忽略缓存强制重新下载
|
||||
Future<File> getFile({
|
||||
required String url,
|
||||
bool ignoreCache = false,
|
||||
}) async {
|
||||
Future<File> getFile({required String url, bool ignoreCache = false}) async {
|
||||
if (ignoreCache) {
|
||||
// 强制重新下载
|
||||
final fileInfo = await _cacheManager.downloadFile(url);
|
||||
@ -82,23 +78,23 @@ class MediaCache {
|
||||
}
|
||||
|
||||
/// 获取缓存文件路径(如果不存在则返回 null)
|
||||
Future<String?> getCachedFilePath(String url) async {
|
||||
Future<String?> getCachedFilePath(String url) async {
|
||||
final fileInfo = await _cacheManager.getFileFromCache(url);
|
||||
return fileInfo?.file.path;
|
||||
}
|
||||
|
||||
/// 清除指定文件的缓存
|
||||
Future<void> removeCachedFile(String url) async {
|
||||
Future<void> removeCachedFile(String url) async {
|
||||
await _cacheManager.removeFile(url);
|
||||
}
|
||||
|
||||
/// 清空所有缓存
|
||||
Future<void> clearAllCache() async {
|
||||
Future<void> clearAllCache() async {
|
||||
await _cacheManager.emptyCache();
|
||||
}
|
||||
|
||||
/// 获取缓存大小(字节)
|
||||
Future<int> getCacheSize() async {
|
||||
Future<int> getCacheSize() async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final cacheDir = Directory('${directory.path}/libCachedData');
|
||||
|
||||
@ -119,19 +115,14 @@ class MediaCache {
|
||||
try {
|
||||
// 1. 将本地文件添加到缓存
|
||||
await cacheManager.putFile(
|
||||
cacheKey, // 缓存键(唯一标识)
|
||||
localFile.readAsBytesSync(), // 文件字节数据
|
||||
fileExtension: localFile.path.split('.').last, // 文件扩展名
|
||||
cacheKey, // 缓存键(唯一标识)
|
||||
localFile.readAsBytesSync(), // 文件字节数据
|
||||
fileExtension: localFile.path.split('.').last, // 文件扩展名
|
||||
);
|
||||
|
||||
// 2. 验证文件已缓存
|
||||
final cachedFile = await cacheManager.getFileFromCache(cacheKey);
|
||||
if (cachedFile != null) {
|
||||
print('文件已成功添加到缓存: ${cachedFile.file.path}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('添加文件到缓存失败: $e');
|
||||
}
|
||||
if (cachedFile != null) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart';
|
||||
@ -21,12 +23,22 @@ import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
typedef FloatingScreenManager = OverlayManager;
|
||||
|
||||
class OverlayManager {
|
||||
static const int _maxLowPerformanceCompensationQueueLength = 8;
|
||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||
static const Duration _compensationDedupWindow = Duration(seconds: 3);
|
||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||
|
||||
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
|
||||
final Map<String, DateTime> _recentCompensationKeys = {};
|
||||
final Map<String, DateTime> _recentFloatingKeys = {};
|
||||
bool _isPlaying = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
bool _homeRootTabsVisible = false;
|
||||
int _suppressedCount = 0;
|
||||
|
||||
bool _isProcessing = false;
|
||||
bool _isDisposed = false;
|
||||
@ -38,27 +50,155 @@ class OverlayManager {
|
||||
OverlayManager._internal();
|
||||
|
||||
void addMessage(SCFloatingMessage message) {
|
||||
if (_isDisposed) return;
|
||||
if (SCGlobalConfig.isFloatingAnimationInGlobal) {
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
if (_isDisposed || _isSuppressed) return;
|
||||
if (!SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
if (_isRecentFloatingDuplicate(message)) {
|
||||
return;
|
||||
}
|
||||
if (message.type == 4) {}
|
||||
_rememberFloatingMessage(message);
|
||||
_enqueueMessage(
|
||||
message,
|
||||
debugLabel: 'floating_message_type_${message.type ?? -1}',
|
||||
action: () {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
_isDisposed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void addLowPerformanceCompensationMessage(
|
||||
SCFloatingMessage message, {
|
||||
String? dedupKey,
|
||||
}) {
|
||||
if (_isDisposed ||
|
||||
_isSuppressed ||
|
||||
!SCGlobalConfig.isLowPerformanceDevice ||
|
||||
SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return;
|
||||
}
|
||||
final key = dedupKey ?? _compensationKeyFor(message);
|
||||
if (_isRecentCompensationDuplicate(key)) {
|
||||
return;
|
||||
}
|
||||
_rememberCompensationKey(key);
|
||||
_trimLowPerformanceCompensationQueue();
|
||||
_enqueueMessage(
|
||||
message,
|
||||
debugLabel: 'low_performance_compensation_${message.type ?? -1}',
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueMessage(
|
||||
SCFloatingMessage message, {
|
||||
required String debugLabel,
|
||||
}) {
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
debugLabel: debugLabel,
|
||||
action: () {
|
||||
if (_isDisposed || _isSuppressed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecentCompensationDuplicate(String key) {
|
||||
_pruneRecentCompensationKeys();
|
||||
final recentTime = _recentCompensationKeys[key];
|
||||
if (recentTime == null) {
|
||||
return false;
|
||||
}
|
||||
return DateTime.now().difference(recentTime) < _compensationDedupWindow;
|
||||
}
|
||||
|
||||
void _rememberCompensationKey(String key) {
|
||||
_recentCompensationKeys[key] = DateTime.now();
|
||||
_pruneRecentCompensationKeys();
|
||||
}
|
||||
|
||||
void _pruneRecentCompensationKeys() {
|
||||
final now = DateTime.now();
|
||||
_recentCompensationKeys.removeWhere(
|
||||
(_, time) => now.difference(time) > _compensationDedupWindow,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecentFloatingDuplicate(SCFloatingMessage message) {
|
||||
_pruneRecentFloatingKeys();
|
||||
final key = _floatingDedupKeyFor(message);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
final recentTime = _recentFloatingKeys[key];
|
||||
if (recentTime == null) {
|
||||
return false;
|
||||
}
|
||||
return DateTime.now().difference(recentTime) <
|
||||
_redPacketFloatingDedupWindow;
|
||||
}
|
||||
|
||||
void _rememberFloatingMessage(SCFloatingMessage message) {
|
||||
final key = _floatingDedupKeyFor(message);
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
_recentFloatingKeys[key] = DateTime.now();
|
||||
_pruneRecentFloatingKeys();
|
||||
}
|
||||
|
||||
void _pruneRecentFloatingKeys() {
|
||||
final now = DateTime.now();
|
||||
_recentFloatingKeys.removeWhere(
|
||||
(_, time) => now.difference(time) > _redPacketFloatingDedupWindow,
|
||||
);
|
||||
}
|
||||
|
||||
String? _floatingDedupKeyFor(SCFloatingMessage message) {
|
||||
if (message.type != 4) {
|
||||
return null;
|
||||
}
|
||||
final packetId = message.toUserId?.trim() ?? '';
|
||||
if (packetId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return 'room_red_packet|$packetId';
|
||||
}
|
||||
|
||||
String _compensationKeyFor(SCFloatingMessage message) {
|
||||
return [
|
||||
message.type,
|
||||
message.roomId,
|
||||
message.userId,
|
||||
message.toUserId,
|
||||
message.giftId,
|
||||
message.number,
|
||||
message.giftUrl,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
void _trimLowPerformanceCompensationQueue() {
|
||||
if (_messageQueue.length < _maxLowPerformanceCompensationQueueLength) {
|
||||
return;
|
||||
}
|
||||
final retained =
|
||||
_messageQueue.unorderedElements.cast<SCFloatingMessage>().toList()
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
_messageQueue.clear();
|
||||
for (final message in retained.take(
|
||||
_maxLowPerformanceCompensationQueueLength - 1,
|
||||
)) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warmMessageImages(SCFloatingMessage message) async {
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
@ -91,12 +231,11 @@ class OverlayManager {
|
||||
await SCSvgaAssetWidget.preloadAsset(
|
||||
FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath,
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('预热幸运礼物横幅SVGA失败: $error');
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
void _safeScheduleNext() {
|
||||
if (_isSuppressed) return;
|
||||
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
|
||||
_isProcessing = true;
|
||||
|
||||
@ -108,7 +247,9 @@ class OverlayManager {
|
||||
}
|
||||
|
||||
void _scheduleNext() {
|
||||
if (_isPlaying || _messageQueue.isEmpty || _isDisposed) return;
|
||||
if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final context = navigatorKey.currentState?.context;
|
||||
@ -117,9 +258,20 @@ class OverlayManager {
|
||||
// 安全地获取第一个消息
|
||||
if (_messageQueue.isEmpty) return;
|
||||
final messageToProcess = _messageQueue.first;
|
||||
if (messageToProcess == null) {
|
||||
_messageQueue.removeFirst();
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_shouldDisplayMessage(context, messageToProcess)) {
|
||||
_messageQueue.removeFirst();
|
||||
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
|
||||
_deferRegionRedPacketMessage(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
_logRegionRedPacketDrop(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
@ -128,7 +280,6 @@ class OverlayManager {
|
||||
final messageToPlay = _messageQueue.removeFirst();
|
||||
_playMessage(messageToPlay);
|
||||
} catch (e) {
|
||||
debugPrint('播放悬浮消息出错: $e');
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
}
|
||||
@ -173,7 +324,6 @@ class OverlayManager {
|
||||
_currentMessage = null;
|
||||
_safeScheduleNext();
|
||||
} catch (e) {
|
||||
debugPrint('清理悬浮消息出错: $e');
|
||||
_isPlaying = false;
|
||||
_currentMessage = null;
|
||||
_safeScheduleNext();
|
||||
@ -224,14 +374,98 @@ class OverlayManager {
|
||||
|
||||
void activate() {
|
||||
_isDisposed = false;
|
||||
_loadSavedDisplayPreference();
|
||||
}
|
||||
|
||||
void _loadSavedDisplayPreference() {
|
||||
final account = AccountStorage().getCurrentUser()?.userProfile?.account;
|
||||
final savedFloatingBroadcastScope = DataPersistence.getString(
|
||||
SCGlobalConfig.floatingBroadcastScopeStorageKey(account),
|
||||
);
|
||||
if (savedFloatingBroadcastScope.isNotEmpty) {
|
||||
final scope = SCGlobalConfig.clampFloatingBroadcastScope(
|
||||
savedFloatingBroadcastScope,
|
||||
);
|
||||
SCGlobalConfig.floatingBroadcastScope = scope;
|
||||
return;
|
||||
}
|
||||
final defaultEffectsEnabled = !SCGlobalConfig.isLowPerformanceDevice;
|
||||
final isLegacyFloatingAnimationInGlobal = DataPersistence.getBool(
|
||||
SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(account),
|
||||
defaultValue: defaultEffectsEnabled,
|
||||
);
|
||||
SCGlobalConfig.floatingBroadcastScope =
|
||||
SCGlobalConfig.clampVisualEffectPreference(
|
||||
isLegacyFloatingAnimationInGlobal,
|
||||
)
|
||||
? SCGlobalConfig.floatingBroadcastScopeAll
|
||||
: SCGlobalConfig.floatingBroadcastScopeOff;
|
||||
}
|
||||
|
||||
void setHomeRootTabsVisible(bool visible) {
|
||||
if (_homeRootTabsVisible == visible) {
|
||||
return;
|
||||
}
|
||||
_homeRootTabsVisible = visible;
|
||||
if (!visible) {
|
||||
_removeActiveRegionBroadcastMessage();
|
||||
_removeRegionBroadcastMessages();
|
||||
return;
|
||||
}
|
||||
_restoreDeferredRegionRedPacketMessages();
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
void beginSuppressFloatingScreens({String reason = 'unknown'}) {
|
||||
_suppressedCount += 1;
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_messageQueue.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
}
|
||||
|
||||
void endSuppressFloatingScreens({String reason = 'unknown'}) {
|
||||
if (_suppressedCount > 0) {
|
||||
_suppressedCount -= 1;
|
||||
}
|
||||
if (!_isSuppressed) {
|
||||
_safeScheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
void refreshDisplayPreference() {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
return;
|
||||
}
|
||||
_restoreDeferredRegionRedPacketMessages();
|
||||
final context = navigatorKey.currentState?.context;
|
||||
final activeMessage = _currentMessage;
|
||||
if (context != null &&
|
||||
context.mounted &&
|
||||
activeMessage != null &&
|
||||
!_shouldDisplayMessage(context, activeMessage)) {
|
||||
_removeActiveMessage();
|
||||
return;
|
||||
}
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_homeRootTabsVisible = false;
|
||||
_suppressedCount = 0;
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_recentCompensationKeys.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
}
|
||||
@ -270,14 +504,100 @@ class OverlayManager {
|
||||
|
||||
bool get isPlaying => _isPlaying;
|
||||
|
||||
bool get _isSuppressed => _suppressedCount > 0;
|
||||
|
||||
bool _isRegionRedPacketMessage(SCFloatingMessage message) {
|
||||
return message.type == 4 && message.isRegionBroadcast;
|
||||
}
|
||||
|
||||
bool _shouldDeferRegionRedPacketMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
if (!_isRegionRedPacketMessage(message) ||
|
||||
SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
return !_shouldDisplayRegionBroadcastMessage(context, message);
|
||||
}
|
||||
|
||||
void _deferRegionRedPacketMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
final key = _floatingDedupKeyFor(message);
|
||||
if (key != null &&
|
||||
_deferredRegionRedPacketMessages.any(
|
||||
(item) => _floatingDedupKeyFor(item) == key,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
while (_deferredRegionRedPacketMessages.length >=
|
||||
_maxDeferredRegionRedPacketCount) {
|
||||
_deferredRegionRedPacketMessages.removeAt(0);
|
||||
}
|
||||
_deferredRegionRedPacketMessages.add(message);
|
||||
}
|
||||
|
||||
void _restoreDeferredRegionRedPacketMessages() {
|
||||
if (_deferredRegionRedPacketMessages.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final messages = List<SCFloatingMessage>.from(
|
||||
_deferredRegionRedPacketMessages,
|
||||
);
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
for (final message in messages) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _logRegionRedPacketDrop(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
if (message.type != 4) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String _displayStateForLog(BuildContext context) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
return 'homeVisible=$_homeRootTabsVisible '
|
||||
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
|
||||
'currentRoomId=$currentRoomId '
|
||||
'scope=${SCGlobalConfig.floatingBroadcastScope}';
|
||||
}
|
||||
|
||||
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
|
||||
if (message == null) {
|
||||
return false;
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
if (message.isRegionBroadcast) {
|
||||
return _shouldDisplayRegionBroadcastMessage(context, message);
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||
return _isCurrentRoomFloatingMessage(context, message);
|
||||
}
|
||||
if (message.type != 0 && message.type != 1 && message.type != 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _isCurrentRoomFloatingMessage(context, message);
|
||||
}
|
||||
|
||||
bool _isCurrentRoomFloatingMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -294,12 +614,64 @@ class OverlayManager {
|
||||
return currentRoomId == messageRoomId;
|
||||
}
|
||||
|
||||
void _removeActiveRoomMessage() {
|
||||
bool _shouldDisplayRegionBroadcastMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
final messageRoomId = (message.roomId ?? "").trim();
|
||||
final isCurrentVisibleRoom =
|
||||
rtcProvider.shouldShowRoomVisualEffects &&
|
||||
currentRoomId.isNotEmpty &&
|
||||
messageRoomId.isNotEmpty &&
|
||||
currentRoomId == messageRoomId;
|
||||
if (isCurrentVisibleRoom) {
|
||||
// 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
|
||||
return message.type == 4;
|
||||
}
|
||||
if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return true;
|
||||
}
|
||||
if (message.type == 4) {
|
||||
return _homeRootTabsVisible;
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||
return false;
|
||||
}
|
||||
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return false;
|
||||
}
|
||||
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
||||
}
|
||||
|
||||
void _removeRegionBroadcastMessages() {
|
||||
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.removeFirst();
|
||||
if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) {
|
||||
newQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
while (newQueue.isNotEmpty) {
|
||||
_messageQueue.add(newQueue.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
void _removeActiveRegionBroadcastMessage() {
|
||||
final activeMessage = _currentMessage;
|
||||
if (activeMessage == null ||
|
||||
(activeMessage.type != 0 &&
|
||||
activeMessage.type != 1 &&
|
||||
activeMessage.type != 5)) {
|
||||
if (activeMessage == null || !activeMessage.isRegionBroadcast) {
|
||||
return;
|
||||
}
|
||||
_currentOverlayEntry?.remove();
|
||||
@ -308,4 +680,25 @@ class OverlayManager {
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
void _removeActiveRoomMessage() {
|
||||
final activeMessage = _currentMessage;
|
||||
if (activeMessage == null ||
|
||||
(activeMessage.type != 0 &&
|
||||
activeMessage.type != 1 &&
|
||||
activeMessage.type != 5)) {
|
||||
return;
|
||||
}
|
||||
_removeActiveMessage();
|
||||
}
|
||||
|
||||
void _removeActiveMessage({bool scheduleNext = true}) {
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
if (scheduleNext) {
|
||||
_safeScheduleNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,8 +300,13 @@ class BaseNetworkClient {
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return DioException(requestOptions: e.requestOptions, error: '接收超时');
|
||||
case DioExceptionType.badResponse:
|
||||
var errorCode = e.response?.data["errorCode"];
|
||||
var errorMsg = e.response?.data["errorMsg"];
|
||||
final responseData = e.response?.data;
|
||||
final errorCode =
|
||||
responseData is Map ? responseData["errorCode"] : null;
|
||||
final errorMsg =
|
||||
responseData is Map
|
||||
? responseData["errorMsg"]
|
||||
: responseData?.toString();
|
||||
if (errorCode == SCErroCode.userNotRegistered.code) {
|
||||
//用户还没有注册
|
||||
SCTts.show("Please register an account first.");
|
||||
@ -319,7 +324,7 @@ class BaseNetworkClient {
|
||||
!SCNavigatorUtils.inLoginPage &&
|
||||
!await _isCurrentSessionStillValid(e);
|
||||
final BuildContext? context = navigatorKey.currentContext;
|
||||
if (context != null && shouldLogout) {
|
||||
if (context != null && context.mounted && shouldLogout) {
|
||||
AccountStorage().logout(context);
|
||||
SCNavigatorUtils.inLoginPage = true;
|
||||
}
|
||||
@ -338,19 +343,22 @@ class BaseNetworkClient {
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
error: '服务器错误: ${e.response?.data["errorCode"]}',
|
||||
error: '服务器错误: $errorCode ${errorMsg ?? ""}'.trim(),
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
type: DioExceptionType.cancel,
|
||||
error: e.error ?? 'Cancel',
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
response: e.response,
|
||||
type: DioExceptionType.cancel,
|
||||
error: e.error ?? 'Cancel',
|
||||
);
|
||||
default:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
error: 'Net fail',
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
error: e.error ?? e.message ?? 'Net fail',
|
||||
message: e.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/sc_logger.dart';
|
||||
|
||||
NetworkClient get http => _httpInstance;
|
||||
|
||||
@ -286,7 +284,6 @@ class NetworkClient extends BaseNetworkClient {
|
||||
// 添加拦截器
|
||||
dio.interceptors
|
||||
..add(ApiInterceptor())
|
||||
..add(SCDioLogger())
|
||||
..add(TimeOutInterceptor());
|
||||
|
||||
setupCacheInterceptor(dio); // 传入 dio 实例
|
||||
@ -441,26 +438,68 @@ class ResponseData<T> {
|
||||
}) {
|
||||
final dynamic responseBody =
|
||||
json.containsKey('body') ? json['body'] : json['data'];
|
||||
final success = _readSuccess(json);
|
||||
final errorCode = _readErrorCode(json);
|
||||
final errorCodeName =
|
||||
json['errorCodeName']?.toString() ?? json['code']?.toString();
|
||||
final errorMsg =
|
||||
json['errorMsg']?.toString() ?? json['message']?.toString();
|
||||
if (fromJsonT == null) {
|
||||
return ResponseData(
|
||||
status: json['status'],
|
||||
errorCode: json['errorCode'],
|
||||
errorCodeName: json['errorCodeName'],
|
||||
errorMsg: json['errorMsg'],
|
||||
status: success,
|
||||
errorCode: errorCode,
|
||||
errorCodeName: errorCodeName,
|
||||
errorMsg: errorMsg,
|
||||
body: responseBody,
|
||||
time: json['time'],
|
||||
);
|
||||
} else {
|
||||
return ResponseData(
|
||||
status: json['status'],
|
||||
errorCode: json['errorCode'],
|
||||
errorCodeName: json['errorCodeName'],
|
||||
errorMsg: json['errorMsg'],
|
||||
status: success,
|
||||
errorCode: errorCode,
|
||||
errorCodeName: errorCodeName,
|
||||
errorMsg: errorMsg,
|
||||
body: responseBody != null ? _parseData(responseBody, fromJsonT) : null,
|
||||
time: json['time'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _readSuccess(Map<String, dynamic> json) {
|
||||
final status = json['status'];
|
||||
if (status is bool) {
|
||||
return status;
|
||||
}
|
||||
if (status is num) {
|
||||
return status != 0;
|
||||
}
|
||||
if (status is String) {
|
||||
final normalized = status.trim().toLowerCase();
|
||||
if (normalized == 'true' || normalized == 'success') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == 'fail') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final code = json['code']?.toString().trim().toLowerCase();
|
||||
return code == 'success' || code == 'ok';
|
||||
}
|
||||
|
||||
static int? _readErrorCode(Map<String, dynamic> json) {
|
||||
final errorCode = json['errorCode'];
|
||||
if (errorCode is int) {
|
||||
return errorCode;
|
||||
}
|
||||
if (errorCode is num) {
|
||||
return errorCode.toInt();
|
||||
}
|
||||
if (errorCode is String) {
|
||||
return int.tryParse(errorCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 超时拦截器
|
||||
@ -487,10 +526,7 @@ class TimeOutInterceptor extends Interceptor {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.receiveTimeout ||
|
||||
err.type == DioExceptionType.sendTimeout) {
|
||||
debugPrint('超时错误: URL => ${err.requestOptions.baseUrl}');
|
||||
debugPrint('当前使用的Host: ${SCGlobalConfig.apiHost}');
|
||||
}
|
||||
err.type == DioExceptionType.sendTimeout) {}
|
||||
|
||||
// 请求出错,取消计时器
|
||||
final String requestKey = err.requestOptions.uri.toString();
|
||||
|
||||
@ -1,267 +0,0 @@
|
||||
/*
|
||||
* @message: 日志拦截器
|
||||
* @Author: Jack
|
||||
* @Email: Jack@163.com
|
||||
* @Date: 2020-06-18 19:47:32
|
||||
*/
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
// 必须引入此库处理 Emoji 截断问题
|
||||
import 'package:characters/characters.dart';
|
||||
|
||||
/// description: Dio 日志拦截器
|
||||
class SCDioLogger extends Interceptor {
|
||||
final bool request;
|
||||
final bool requestHeader;
|
||||
final bool requestBody;
|
||||
final bool responseBody;
|
||||
final bool responseHeader;
|
||||
final bool error;
|
||||
static const int initialTab = 1;
|
||||
static const String tabStep = ' ';
|
||||
final bool compact;
|
||||
final int maxWidth;
|
||||
|
||||
/// Log printer; defaults debugPrint to console.
|
||||
void Function(Object object) logPrint;
|
||||
final bool enableLog;
|
||||
|
||||
SCDioLogger({
|
||||
this.enableLog = kDebugMode,
|
||||
this.request = true,
|
||||
this.requestHeader = true,
|
||||
this.requestBody = true,
|
||||
this.responseHeader = false,
|
||||
this.responseBody = true,
|
||||
this.error = true,
|
||||
this.maxWidth = 90,
|
||||
this.compact = true,
|
||||
this.logPrint = print, // 修复:在 iOS 上 debugPrint 比 print 更稳健
|
||||
});
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (!enableLog) return handler.next(options);
|
||||
|
||||
if (request) _pReqHdr(options);
|
||||
if (requestHeader) {
|
||||
_pTable(options.queryParameters, header: 'Query Parameters');
|
||||
final requestHeaders = <String, dynamic>{};
|
||||
requestHeaders.addAll(options.headers);
|
||||
requestHeaders['contentType'] = options.contentType?.toString();
|
||||
requestHeaders['responseType'] = options.responseType?.toString();
|
||||
requestHeaders['followRedirects'] = options.followRedirects;
|
||||
requestHeaders['connectTimeout'] = options.connectTimeout;
|
||||
requestHeaders['receiveTimeout'] = options.receiveTimeout;
|
||||
_pTable(requestHeaders, header: 'Headers');
|
||||
_pTable(options.extra, header: 'Extras');
|
||||
}
|
||||
if (requestBody && options.method != 'GET') {
|
||||
final data = options.data;
|
||||
if (data != null) {
|
||||
if (data is Map) {
|
||||
_pTable(data, header: 'Body');
|
||||
} else if (data is FormData) {
|
||||
final formDataMap = Map()
|
||||
..addEntries(data.fields)
|
||||
..addEntries(data.files);
|
||||
_pTable(formDataMap, header: 'Form data | ${data.boundary}');
|
||||
} else {
|
||||
_pBlock(data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (!enableLog) return handler.next(err);
|
||||
|
||||
if (error) {
|
||||
if (err.type == DioExceptionType.badResponse) {
|
||||
final uri = err.response?.requestOptions.uri;
|
||||
_pBox(
|
||||
header: 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}',
|
||||
text: uri.toString());
|
||||
if (err.response?.data != null) {
|
||||
logPrint('╔ ${err.type.toString()}');
|
||||
_pResp(err.response!);
|
||||
}
|
||||
_pLine('╚');
|
||||
} else {
|
||||
final uri = err.requestOptions.uri;
|
||||
_pBox(header: 'DioError ║ ${err.type} ║ ${uri.toString()}', text: err.message);
|
||||
}
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (!enableLog) return handler.next(response);
|
||||
|
||||
if (responseHeader) {
|
||||
final responseHeaders = <String, String>{};
|
||||
response.headers.forEach((k, list) => responseHeaders[k] = list.toString());
|
||||
_pTable(responseHeaders, header: 'Headers');
|
||||
}
|
||||
|
||||
if (responseBody) {
|
||||
_pRespHdr(response);
|
||||
logPrint('╔ Body');
|
||||
logPrint('║');
|
||||
_pResp(response);
|
||||
logPrint('║');
|
||||
_pLine('╚');
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
void _pBox({String? header, String? text}) {
|
||||
logPrint('');
|
||||
logPrint('╔╣ $header');
|
||||
logPrint('║ $text');
|
||||
_pLine('╚');
|
||||
}
|
||||
|
||||
void _pResp(Response response) {
|
||||
if (response.data != null) {
|
||||
if (response.data is Map) {
|
||||
_pMap(response.data);
|
||||
} else if (response.data is List) {
|
||||
logPrint('║${_ind()}[');
|
||||
_pList(response.data);
|
||||
logPrint('║${_ind()}]');
|
||||
} else {
|
||||
_pBlock(response.data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _pRespHdr(Response response) {
|
||||
final uri = response.requestOptions.uri;
|
||||
final method = response.requestOptions.method;
|
||||
String header = 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}';
|
||||
logPrint('╔╣ $header ${'═' * 20}');
|
||||
logPrint('║ ${uri.toString()}');
|
||||
logPrint('║ ');
|
||||
}
|
||||
|
||||
void _pReqHdr(RequestOptions options) {
|
||||
final uri = options.uri;
|
||||
final method = options.method;
|
||||
_pBox(header: 'Request ║ $method ', text: uri.toString());
|
||||
}
|
||||
|
||||
void _pLine([String pre = '', String suf = '╝']) => logPrint('$pre${'═' * maxWidth}');
|
||||
|
||||
void _pKV(String key, Object v) {
|
||||
final pre = '╟ $key: ';
|
||||
final msg = v.toString();
|
||||
|
||||
if (pre.length + msg.length > maxWidth) {
|
||||
logPrint(pre);
|
||||
_pBlock(msg);
|
||||
} else {
|
||||
logPrint('$pre$msg');
|
||||
}
|
||||
}
|
||||
|
||||
// 修复:使用 characters 处理截断,防止 Emoji 损坏
|
||||
void _pBlock(String msg) {
|
||||
final charData = msg.characters;
|
||||
int lines = (charData.length / maxWidth).ceil();
|
||||
for (int i = 0; i < lines; ++i) {
|
||||
final start = i * maxWidth;
|
||||
final end = math.min<int>(start + maxWidth, charData.length);
|
||||
logPrint('║ ' + charData.getRange(start, end).toString());
|
||||
}
|
||||
}
|
||||
|
||||
String _ind([int tabCount = initialTab]) => tabStep * tabCount;
|
||||
|
||||
void _pMap(Map data, {int tabs = initialTab, bool isListItem = false, bool isLast = false}) {
|
||||
final bool isRoot = tabs == initialTab;
|
||||
final initialIndent = _ind(tabs);
|
||||
tabs++;
|
||||
|
||||
if (isRoot || isListItem) logPrint('║$initialIndent{');
|
||||
|
||||
data.keys.toList().asMap().forEach((index, key) {
|
||||
final isLast = index == data.length - 1;
|
||||
var value = data[key];
|
||||
if (value is String) value = '\"$value\"';
|
||||
|
||||
if (value is Map) {
|
||||
if (compact && _flatMap(value)) {
|
||||
logPrint('║${_ind(tabs)} $key: $value${!isLast ? ',' : ''}');
|
||||
} else {
|
||||
logPrint('║${_ind(tabs)} $key: {');
|
||||
_pMap(value, tabs: tabs);
|
||||
}
|
||||
} else if (value is List) {
|
||||
if (compact && _flatList(value)) {
|
||||
logPrint('║${_ind(tabs)} $key: ${value.toString()}');
|
||||
} else {
|
||||
logPrint('║${_ind(tabs)} $key: [');
|
||||
_pList(value, tabs: tabs);
|
||||
logPrint('║${_ind(tabs)} ]${isLast ? '' : ','}');
|
||||
}
|
||||
} else {
|
||||
final msg = value.toString().replaceAll('\n', '');
|
||||
final indent = _ind(tabs);
|
||||
final charMsg = msg.characters;
|
||||
final linWidth = maxWidth - indent.length;
|
||||
|
||||
// 修复:字符串分段时处理 Emoji
|
||||
if (charMsg.length + indent.length > maxWidth) {
|
||||
int lines = (charMsg.length / linWidth).ceil();
|
||||
for (int i = 0; i < lines; ++i) {
|
||||
final start = i * linWidth;
|
||||
final end = math.min<int>(start + linWidth, charMsg.length);
|
||||
logPrint('║${_ind(tabs)} ${charMsg.getRange(start, end).toString()}');
|
||||
}
|
||||
} else {
|
||||
logPrint('║${_ind(tabs)} $key: $msg${!isLast ? ',' : ''}');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}');
|
||||
}
|
||||
|
||||
void _pList(List list, {int tabs = initialTab}) {
|
||||
list.asMap().forEach((i, e) {
|
||||
final isLast = i == list.length - 1;
|
||||
if (e is Map) {
|
||||
if (compact && _flatMap(e)) {
|
||||
logPrint('║${_ind(tabs)} $e${!isLast ? ',' : ''}');
|
||||
} else {
|
||||
_pMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast);
|
||||
}
|
||||
} else {
|
||||
logPrint('║${_ind(tabs + 2)} $e${isLast ? '' : ','}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _flatMap(Map map) {
|
||||
return map.values.where((val) => val is Map || val is List).isEmpty &&
|
||||
map.toString().length < maxWidth;
|
||||
}
|
||||
|
||||
bool _flatList(List list) {
|
||||
return (list.length < 10 && list.toString().length < maxWidth);
|
||||
}
|
||||
|
||||
void _pTable(Map? map, {String? header}) {
|
||||
if (map == null || map.isEmpty) return;
|
||||
logPrint('╔ $header ');
|
||||
map.forEach((key, value) {
|
||||
_pKV(key.toString(), (value ?? 'null').toString());
|
||||
});
|
||||
_pLine('╚');
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
@ -10,7 +14,10 @@ import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/config_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'
|
||||
show BaseNetworkClient;
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||
|
||||
class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
static SCConfigRepositoryImp? _instance;
|
||||
@ -48,6 +55,25 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///go/app/popups/entry
|
||||
@override
|
||||
Future<SCEntryPopupRes> entryPopup() async {
|
||||
final deviceId = await SCDeviceIdUtils.getDeviceId();
|
||||
final result = await http.post<SCEntryPopupRes>(
|
||||
"/go/app/popups/entry",
|
||||
data: {
|
||||
"scene": "APP_ENTER",
|
||||
"platform": Platform.isIOS ? "ios" : "android",
|
||||
"appVersion": SCGlobalConfig.version,
|
||||
"deviceId": deviceId,
|
||||
"language": SCGlobalConfig.lang,
|
||||
},
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCEntryPopupRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///sys/config/enum/config
|
||||
@override
|
||||
Future<CountryRes> getConfig() async {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/general_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
@ -21,8 +20,6 @@ class SCGeneralRepositoryImp implements SocialChatGeneralRepository {
|
||||
filePath.lastIndexOf("/") + 1,
|
||||
filePath.length,
|
||||
);
|
||||
debugPrint("[上传头像图片地址] filePath: $filePath");
|
||||
debugPrint("[上传头像图片] fileName: $name");
|
||||
FormData formData = FormData.fromMap({
|
||||
"file": await MultipartFile.fromFile(filePath, filename: name),
|
||||
});
|
||||
@ -40,8 +37,6 @@ class SCGeneralRepositoryImp implements SocialChatGeneralRepository {
|
||||
response.data as Map<String, dynamic>,
|
||||
fromJsonT: (json) => json as String,
|
||||
);
|
||||
debugPrint("[返回值] response.data: ${response.data}");
|
||||
debugPrint("[返回图片地址] parsed file url: ${baseResponse.body ?? ""}");
|
||||
return baseResponse.body ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart';
|
||||
@ -27,7 +29,9 @@ import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_record_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/room_repository.dart';
|
||||
@ -45,8 +49,65 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
return _instance ??= SCChatRoomRepository._internal();
|
||||
}
|
||||
|
||||
void _giftRepoLog(String message) {
|
||||
debugPrint('[GiftFX][Repo] $message');
|
||||
void _giftRepoLog(String message) {}
|
||||
|
||||
Future<T> _traceRoomRedPacketApi<T>(
|
||||
String method,
|
||||
String path,
|
||||
Future<T> Function() request, {
|
||||
dynamic data,
|
||||
dynamic query,
|
||||
}) async {
|
||||
final startedAt = DateTime.now();
|
||||
try {
|
||||
final result = await request();
|
||||
return result;
|
||||
} catch (error, stackTrace) {
|
||||
_logRoomRedPacketApiError(method, path, error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _logRoomRedPacketApiError(
|
||||
String method,
|
||||
String path,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
) {
|
||||
if (error is DioException) {
|
||||
final request = error.requestOptions;
|
||||
} else {}
|
||||
}
|
||||
|
||||
String _debugJson(dynamic value) {
|
||||
if (value == null) {
|
||||
return '-';
|
||||
}
|
||||
try {
|
||||
return jsonEncode(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String _debugResult(dynamic value) {
|
||||
if (value == null) {
|
||||
return 'null';
|
||||
}
|
||||
if (value is List) {
|
||||
return 'List(length=${value.length})';
|
||||
}
|
||||
try {
|
||||
final dynamic json = value.toJson();
|
||||
return '${value.runtimeType} ${_debugJson(json)}';
|
||||
} catch (_) {
|
||||
return value.runtimeType.toString();
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _jsonInt64(String value) {
|
||||
final normalized = value.trim();
|
||||
return int.tryParse(normalized) ?? normalized;
|
||||
}
|
||||
|
||||
bool _isBrokenLocalMediaUrl(String? url) {
|
||||
@ -63,19 +124,17 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
if (!_shouldHydrateRoomProfile(cachedRoom.id, cachedRoom.roomCover)) {
|
||||
return cachedRoom;
|
||||
}
|
||||
debugPrint(
|
||||
"[Room Cover][room-list] hydrate roomId=${cachedRoom.id} remoteCover=${cachedRoom.roomCover ?? ""}",
|
||||
);
|
||||
final specificRoom = await specific(cachedRoom.id ?? "");
|
||||
debugPrint(
|
||||
"[Room Cover][room-list] specific roomId=${cachedRoom.id} specificCover=${specificRoom.roomCover ?? ""}",
|
||||
);
|
||||
return SCRoomProfileCache.applyToSocialChatRoomRes(
|
||||
cachedRoom.copyWith(
|
||||
roomCover: SCRoomProfileCache.preferNonEmpty(
|
||||
specificRoom.roomCover,
|
||||
cachedRoom.roomCover,
|
||||
),
|
||||
roomBackground: SCRoomProfileCache.preferNonEmpty(
|
||||
specificRoom.roomBackground,
|
||||
cachedRoom.roomBackground,
|
||||
),
|
||||
roomName: SCRoomProfileCache.preferNonEmpty(
|
||||
specificRoom.roomName,
|
||||
cachedRoom.roomName,
|
||||
@ -136,12 +195,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
queryParams: queryParams,
|
||||
fromJson: (json) => MyRoomRes.fromJson(json),
|
||||
);
|
||||
debugPrint(
|
||||
"[Room Cover][specific] roomId=${result.id ?? roomId} roomCover=${result.roomCover ?? ""} roomName=${result.roomName ?? ""}",
|
||||
);
|
||||
await SCRoomProfileCache.saveRoomProfile(
|
||||
roomId: result.id ?? roomId,
|
||||
roomCover: result.roomCover,
|
||||
roomBackground: result.roomBackground,
|
||||
roomName: result.roomName,
|
||||
roomDesc: result.roomDesc,
|
||||
);
|
||||
@ -196,8 +253,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
num mickIndex, {
|
||||
String? eventType,
|
||||
String? inviterId,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
params["roomId"] = roomId;
|
||||
params["mickIndex"] = mickIndex;
|
||||
if (eventType != null) {
|
||||
@ -262,12 +319,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
silentErrorToast
|
||||
? const {BaseNetworkClient.silentErrorToastKey: true}
|
||||
: null,
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
|
||||
.toList(),
|
||||
);
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
|
||||
.toList(),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -316,8 +373,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||
String? dynamicContentId,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
if (roomId != null) {
|
||||
params["roomId"] = roomId;
|
||||
}
|
||||
@ -512,10 +569,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
bool? adminLockSeat,
|
||||
bool? showHeartbeat,
|
||||
bool? openKtvMode,
|
||||
String? roomSpecialMikeType,
|
||||
}) async {
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
Map<String, dynamic> params = {};
|
||||
String? roomSpecialMikeType,
|
||||
}) async {
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
Map<String, dynamic> params = {};
|
||||
params["roomId"] = roomId;
|
||||
if (touristMike != null) {
|
||||
params["touristMike"] = touristMike;
|
||||
@ -553,7 +610,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
fromJson: (json) => json as bool,
|
||||
);
|
||||
|
||||
rtmProvider.dispatchMessage(
|
||||
rtmProvider.dispatchMessage(
|
||||
Msg(
|
||||
groupId: roomAcount,
|
||||
msg: roomId,
|
||||
@ -773,16 +830,22 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
parm["roomId"] = roomId;
|
||||
parm["current"] = current;
|
||||
parm["size"] = size;
|
||||
final result = await http.get<List<SCRoomRedPacketListRes>>(
|
||||
"318de84592dca60c287ba2f54d60756abc4a9e8c4704c0f73c085e7ab50e7fdb",
|
||||
queryParams: parm,
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => SCRoomRedPacketListRes.fromJson(e))
|
||||
.toList(),
|
||||
const path =
|
||||
"318de84592dca60c287ba2f54d60756abc4a9e8c4704c0f73c085e7ab50e7fdb";
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<List<SCRoomRedPacketListRes>>(
|
||||
path,
|
||||
queryParams: parm,
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List)
|
||||
.map((e) => SCRoomRedPacketListRes.fromJson(e))
|
||||
.toList(),
|
||||
),
|
||||
query: parm,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///room/red-packet/create
|
||||
@ -791,45 +854,190 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String roomId,
|
||||
String totalAmount,
|
||||
String totalCount,
|
||||
String expireMinutes,
|
||||
) async {
|
||||
String expireMinutes, {
|
||||
String packetMode = 'DELAYED',
|
||||
}) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["roomId"] = roomId;
|
||||
parm["totalAmount"] = totalAmount;
|
||||
parm["totalCount"] = totalCount;
|
||||
parm["expireMinutes"] = expireMinutes;
|
||||
final result = await http.post<SCRoomRedPacketListRes>(
|
||||
"318de84592dca60c287ba2f54d60756af01ca103e12cc332b59b5a8e39c778ec",
|
||||
parm["requestId"] =
|
||||
"room-red-packet-${DateTime.now().microsecondsSinceEpoch}";
|
||||
parm["roomId"] = _jsonInt64(roomId);
|
||||
parm["packetMode"] = packetMode;
|
||||
parm["totalAmount"] = _jsonInt64(totalAmount);
|
||||
parm["totalCount"] = _jsonInt64(totalCount);
|
||||
const path = "/go/app/voice-room/red-packet/send";
|
||||
return _traceRoomRedPacketApi(
|
||||
'POST',
|
||||
path,
|
||||
() => http.post<SCRoomRedPacketListRes>(
|
||||
path,
|
||||
data: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketListRes.fromJson(json),
|
||||
),
|
||||
data: parm,
|
||||
fromJson: (json) => SCRoomRedPacketListRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///room/red-packet/grab
|
||||
@override
|
||||
Future<SCRoomRedPacketGrabRes> roomRedPacketGrab(String packetId) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["packetId"] = packetId;
|
||||
final result = await http.post<SCRoomRedPacketGrabRes>(
|
||||
"318de84592dca60c287ba2f54d60756a0b2a61d0ce4f3f4d50875dcc95b640af",
|
||||
parm["packetNo"] = packetId;
|
||||
const path = "/go/app/voice-room/red-packet/claim";
|
||||
return _traceRoomRedPacketApi(
|
||||
'POST',
|
||||
path,
|
||||
() => http.post<SCRoomRedPacketGrabRes>(
|
||||
path,
|
||||
data: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketGrabRes.fromJson(json),
|
||||
),
|
||||
data: parm,
|
||||
fromJson: (json) => SCRoomRedPacketGrabRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///room/red-packet/detail
|
||||
@override
|
||||
Future<SCRoomRedPacketDetailRes> roomRedPacketDetail(String packetId) async {
|
||||
Map<String, dynamic> parm = {};
|
||||
parm["packetId"] = packetId;
|
||||
final result = await http.get<SCRoomRedPacketDetailRes>(
|
||||
"318de84592dca60c287ba2f54d60756a7f342deecb1b83d2d8429c59146f10f0",
|
||||
queryParams: parm,
|
||||
fromJson: (json) => SCRoomRedPacketDetailRes.fromJson(json),
|
||||
parm["packetNo"] = packetId;
|
||||
const path = "/go/app/voice-room/red-packet/detail";
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<SCRoomRedPacketDetailRes>(
|
||||
path,
|
||||
queryParams: parm,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketDetailRes.fromJson(json),
|
||||
),
|
||||
query: parm,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketConfigRes> roomRedPacketConfig() async {
|
||||
const path = "/go/app/voice-room/red-packet/config";
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<SCRoomRedPacketConfigRes>(
|
||||
path,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketConfigRes.fromJson(json),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketImGroupRes> roomRedPacketImGroup() async {
|
||||
const path = "/go/app/voice-room/red-packet/im-group";
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<SCRoomRedPacketImGroupRes>(
|
||||
path,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketImGroupRes.fromJson(json),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceHeartbeat(
|
||||
String roomId,
|
||||
) async {
|
||||
const path = "/go/app/voice-room/red-packet/presence/heartbeat";
|
||||
final data = {"roomId": _jsonInt64(roomId)};
|
||||
return _traceRoomRedPacketApi(
|
||||
'POST',
|
||||
path,
|
||||
() => http.post<SCRoomRedPacketPresenceRes>(
|
||||
path,
|
||||
data: data,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketPresenceRes.fromJson(json),
|
||||
),
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketPresenceRes> roomRedPacketPresenceLeave(
|
||||
String roomId,
|
||||
) async {
|
||||
const path = "/go/app/voice-room/red-packet/presence/leave";
|
||||
final data = {"roomId": _jsonInt64(roomId)};
|
||||
return _traceRoomRedPacketApi(
|
||||
'POST',
|
||||
path,
|
||||
() => http.post<SCRoomRedPacketPresenceRes>(
|
||||
path,
|
||||
data: data,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketPresenceRes.fromJson(json),
|
||||
),
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SCRoomRedPacketSentRecordRes>> roomRedPacketSentRecords({
|
||||
int cursor = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
const path = "/go/app/voice-room/red-packet/sent-records";
|
||||
final query = {"cursor": cursor, "limit": limit};
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<List<SCRoomRedPacketSentRecordRes>>(
|
||||
path,
|
||||
queryParams: query,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson:
|
||||
(json) =>
|
||||
_redPacketRecordListBody(
|
||||
json,
|
||||
).map((e) => SCRoomRedPacketSentRecordRes.fromJson(e)).toList(),
|
||||
),
|
||||
query: query,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCRoomRedPacketClaimRecordsRes> roomRedPacketClaimRecords(
|
||||
String packetNo,
|
||||
) async {
|
||||
const path = "/go/app/voice-room/red-packet/claim-records";
|
||||
final query = {"packetNo": packetNo};
|
||||
return _traceRoomRedPacketApi(
|
||||
'GET',
|
||||
path,
|
||||
() => http.get<SCRoomRedPacketClaimRecordsRes>(
|
||||
path,
|
||||
queryParams: query,
|
||||
extra: {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCRoomRedPacketClaimRecordsRes.fromJson(json),
|
||||
),
|
||||
query: query,
|
||||
);
|
||||
}
|
||||
|
||||
List<dynamic> _redPacketRecordListBody(dynamic json) {
|
||||
if (json is List) {
|
||||
return json;
|
||||
}
|
||||
if (json is Map) {
|
||||
for (final key in const ['records', 'list', 'items']) {
|
||||
final value = json[key];
|
||||
if (value is List) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return const <dynamic>[];
|
||||
}
|
||||
|
||||
///activity/room-contribution-activity/room-reward/info
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user