feat: update home room and chat experience
@ -138,7 +138,7 @@
|
|||||||
android:name=".VoiceRoomForegroundService"
|
android:name=".VoiceRoomForegroundService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback|microphone"
|
android:foregroundServiceType="mediaPlayback|microphone"
|
||||||
android:stopWithTask="false" />
|
android:stopWithTask="true" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
|||||||
@ -10,12 +10,14 @@ import android.content.Intent
|
|||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import com.tencent.trtc.TRTCCloud
|
||||||
|
|
||||||
class VoiceRoomForegroundService : Service() {
|
class VoiceRoomForegroundService : Service() {
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
explicitStopRequested = false
|
||||||
ensureNotificationChannel()
|
ensureNotificationChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +30,32 @@ class VoiceRoomForegroundService : Service() {
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (!explicitStopRequested) {
|
||||||
|
releaseRtcAfterTaskRemoval()
|
||||||
|
}
|
||||||
|
explicitStopRequested = false
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun releaseRtcAfterTaskRemoval() {
|
||||||
|
try {
|
||||||
|
TRTCCloud.sharedInstance(applicationContext).apply {
|
||||||
|
muteLocalAudio(true)
|
||||||
|
stopLocalAudio()
|
||||||
|
exitRoom()
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
TRTCCloud.destroySharedInstance()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startAsForeground(roomName: String) {
|
private fun startAsForeground(roomName: String) {
|
||||||
val notification = buildNotification(roomName)
|
val notification = buildNotification(roomName)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@ -116,6 +144,9 @@ class VoiceRoomForegroundService : Service() {
|
|||||||
private const val EXTRA_ROOM_NAME = "room_name"
|
private const val EXTRA_ROOM_NAME = "room_name"
|
||||||
private const val DEFAULT_ROOM_NAME = "Voice room is running"
|
private const val DEFAULT_ROOM_NAME = "Voice room is running"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var explicitStopRequested = false
|
||||||
|
|
||||||
fun start(context: Context, roomName: String?) {
|
fun start(context: Context, roomName: String?) {
|
||||||
val intent = Intent(context, VoiceRoomForegroundService::class.java)
|
val intent = Intent(context, VoiceRoomForegroundService::class.java)
|
||||||
.putExtra(EXTRA_ROOM_NAME, roomName)
|
.putExtra(EXTRA_ROOM_NAME, roomName)
|
||||||
@ -127,6 +158,7 @@ class VoiceRoomForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
|
explicitStopRequested = true
|
||||||
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
|
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,3 +7,7 @@ android.enableJetifier=true
|
|||||||
systemProp.https.protocols=TLSv1.2
|
systemProp.https.protocols=TLSv1.2
|
||||||
systemProp.jdk.tls.client.protocols=TLSv1.2
|
systemProp.jdk.tls.client.protocols=TLSv1.2
|
||||||
systemProp.java.net.preferIPv4Stack=true
|
systemProp.java.net.preferIPv4Stack=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
@ -129,6 +129,7 @@
|
|||||||
"redEnvelopeNotYetClaimed": "المظروف الأحمر لم يُستلم بعد",
|
"redEnvelopeNotYetClaimed": "المظروف الأحمر لم يُستلم بعد",
|
||||||
"redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات",
|
"redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات",
|
||||||
"followed": "تمت المتابعة",
|
"followed": "تمت المتابعة",
|
||||||
|
"joined": "انضممت",
|
||||||
"newMessage": "رسالة جديدة",
|
"newMessage": "رسالة جديدة",
|
||||||
"keep": "الاحتفاظ",
|
"keep": "الاحتفاظ",
|
||||||
"open": "فتح",
|
"open": "فتح",
|
||||||
@ -233,7 +234,8 @@
|
|||||||
"alreadyAnTourist": "أنت بالفعل سائح",
|
"alreadyAnTourist": "أنت بالفعل سائح",
|
||||||
"deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟",
|
"deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟",
|
||||||
"account": "الحساب",
|
"account": "الحساب",
|
||||||
"youHaventFollowed": "أنت لم تتابع أي غرفة",
|
"youHaventFollowed": "أنت لم تتابع أي غرفة",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "لم أنضم إلى أي غرف بعد.",
|
||||||
"messageHasBeenRecalled": "تم استرجاع هذه الرسالة",
|
"messageHasBeenRecalled": "تم استرجاع هذه الرسالة",
|
||||||
"propMessagePrompt": "موجه رسالة دعائية",
|
"propMessagePrompt": "موجه رسالة دعائية",
|
||||||
"deleteFromMyDevice": "احذف من جهازي",
|
"deleteFromMyDevice": "احذف من جهازي",
|
||||||
@ -559,7 +561,8 @@
|
|||||||
"coinsReceived": "العملات المستلمة",
|
"coinsReceived": "العملات المستلمة",
|
||||||
"hotRooms": "الغرف الرائجة",
|
"hotRooms": "الغرف الرائجة",
|
||||||
"viewMore": "عرض المزيد",
|
"viewMore": "عرض المزيد",
|
||||||
"noData": "لا توجد بيانات",
|
"noData": "لا توجد بيانات",
|
||||||
|
"noRoomRecordsRecently": "لا توجد سجلات غرف حديثة.",
|
||||||
"recommend": "موصى به",
|
"recommend": "موصى به",
|
||||||
"follow": "متابعة",
|
"follow": "متابعة",
|
||||||
"monthly": "شهريًا",
|
"monthly": "شهريًا",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
|
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
|
||||||
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
|
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
|
||||||
"launchRocket": "إطلاق الصاروخ",
|
"launchRocket": "إطلاق الصاروخ",
|
||||||
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!"
|
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!",
|
||||||
|
"leaderboardWealth": "الثروة",
|
||||||
|
"leaderboardCharm": "الجاذبية",
|
||||||
|
"leaderboardRoom": "الغرفة"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,6 +258,7 @@
|
|||||||
"youDontHaveAnyCouponsYet": "আপনার এখনও কোনো কুপন নেই।",
|
"youDontHaveAnyCouponsYet": "আপনার এখনও কোনো কুপন নেই।",
|
||||||
"recall": "প্রত্যাহার করুন",
|
"recall": "প্রত্যাহার করুন",
|
||||||
"youHaventFollowed": "আপনি কোনো রুম ফলো করেন না",
|
"youHaventFollowed": "আপনি কোনো রুম ফলো করেন না",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "আমি এখনও কোনো রুমে যোগ দিইনি।",
|
||||||
"deleteFromMyDevice": "আমার ডিভাইস থেকে মুছে ফেলুন",
|
"deleteFromMyDevice": "আমার ডিভাইস থেকে মুছে ফেলুন",
|
||||||
"deleteOnAllDevices": "সমস্ত ডিভাইসে মুছে ফেলুন",
|
"deleteOnAllDevices": "সমস্ত ডিভাইসে মুছে ফেলুন",
|
||||||
"messageHasBeenRecalled": "এই বার্তাটি প্রত্যাহার করা হয়েছে",
|
"messageHasBeenRecalled": "এই বার্তাটি প্রত্যাহার করা হয়েছে",
|
||||||
@ -387,6 +388,7 @@
|
|||||||
"hotRooms": "হট রুম",
|
"hotRooms": "হট রুম",
|
||||||
"viewMore": "আরও দেখুন",
|
"viewMore": "আরও দেখুন",
|
||||||
"noData": "ডেটা নেই",
|
"noData": "ডেটা নেই",
|
||||||
|
"noRoomRecordsRecently": "সম্প্রতি কোনো রুমের রেকর্ড নেই।",
|
||||||
"users": "ব্যবহারকারী",
|
"users": "ব্যবহারকারী",
|
||||||
"rooms": "রুম",
|
"rooms": "রুম",
|
||||||
"coins": "কয়েন",
|
"coins": "কয়েন",
|
||||||
@ -646,6 +648,7 @@
|
|||||||
"purchaseIsSuccessful": "ক্রয় সফল",
|
"purchaseIsSuccessful": "ক্রয় সফল",
|
||||||
"buy": "কিনুন",
|
"buy": "কিনুন",
|
||||||
"followed": "ফলো করা হয়েছে",
|
"followed": "ফলো করা হয়েছে",
|
||||||
|
"joined": "যোগদান করা",
|
||||||
"follow2": "ফলো:{1}",
|
"follow2": "ফলো:{1}",
|
||||||
"fans2": "ফ্যান:{1}",
|
"fans2": "ফ্যান:{1}",
|
||||||
"vistors2": "ভিজিটর:{1}",
|
"vistors2": "ভিজিটর:{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
|
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
|
||||||
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
|
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
|
||||||
"launchRocket": "রকেট চালু করুন",
|
"launchRocket": "রকেট চালু করুন",
|
||||||
"roomRocketFuelFullNotice": "রকেটের জ্বালানি পূর্ণ! {1} সেকেন্ড পরে উৎক্ষেপণ!"
|
"roomRocketFuelFullNotice": "রকেটের জ্বালানি পূর্ণ! {1} সেকেন্ড পরে উৎক্ষেপণ!",
|
||||||
|
"leaderboardWealth": "সম্পদ",
|
||||||
|
"leaderboardCharm": "আকর্ষণ",
|
||||||
|
"leaderboardRoom": "রুম"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,6 +228,7 @@
|
|||||||
"youDontHaveAnyCouponsYet": "You don't have any coupons yet.",
|
"youDontHaveAnyCouponsYet": "You don't have any coupons yet.",
|
||||||
"recall": "Recall",
|
"recall": "Recall",
|
||||||
"youHaventFollowed": "You haven't followed any room",
|
"youHaventFollowed": "You haven't followed any room",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "I haven't joined any rooms yet.",
|
||||||
"deleteFromMyDevice": "Delete from my device",
|
"deleteFromMyDevice": "Delete from my device",
|
||||||
"deleteOnAllDevices": "Delete on all devices",
|
"deleteOnAllDevices": "Delete on all devices",
|
||||||
"messageHasBeenRecalled": "This message has been recalled",
|
"messageHasBeenRecalled": "This message has been recalled",
|
||||||
@ -357,6 +358,7 @@
|
|||||||
"hotRooms": "Hot Rooms",
|
"hotRooms": "Hot Rooms",
|
||||||
"viewMore": "View more",
|
"viewMore": "View more",
|
||||||
"noData": "No data",
|
"noData": "No data",
|
||||||
|
"noRoomRecordsRecently": "No room records recently.",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms",
|
||||||
"coins": "coins",
|
"coins": "coins",
|
||||||
@ -646,6 +648,7 @@
|
|||||||
"purchaseIsSuccessful": "Purchase is successful",
|
"purchaseIsSuccessful": "Purchase is successful",
|
||||||
"buy": "Buy",
|
"buy": "Buy",
|
||||||
"followed": "Followed",
|
"followed": "Followed",
|
||||||
|
"joined": "Joined",
|
||||||
"follow2": "Follow:{1}",
|
"follow2": "Follow:{1}",
|
||||||
"fans2": "Fans:{1}",
|
"fans2": "Fans:{1}",
|
||||||
"vistors2": "Visitors:{1}",
|
"vistors2": "Visitors:{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "No reward",
|
"roomRocketRewardEmptyText": "No reward",
|
||||||
"roomRocketRecordEmptyText": "No reward records",
|
"roomRocketRecordEmptyText": "No reward records",
|
||||||
"launchRocket": "Launch Rocket",
|
"launchRocket": "Launch Rocket",
|
||||||
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!"
|
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!",
|
||||||
|
"leaderboardWealth": "Wealth",
|
||||||
|
"leaderboardCharm": "Charm",
|
||||||
|
"leaderboardRoom": "Room"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,6 +228,7 @@
|
|||||||
"youDontHaveAnyCouponsYet": "Vous n'avez pas encore de coupons.",
|
"youDontHaveAnyCouponsYet": "Vous n'avez pas encore de coupons.",
|
||||||
"recall": "Rappel",
|
"recall": "Rappel",
|
||||||
"youHaventFollowed": "Vous n'avez suivi aucune pièce",
|
"youHaventFollowed": "Vous n'avez suivi aucune pièce",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "Je n’ai encore rejoint aucune salle.",
|
||||||
"deleteFromMyDevice": "Supprimer de mon appareil",
|
"deleteFromMyDevice": "Supprimer de mon appareil",
|
||||||
"deleteOnAllDevices": "Supprimer sur tous les appareils",
|
"deleteOnAllDevices": "Supprimer sur tous les appareils",
|
||||||
"messageHasBeenRecalled": "Ce message a été rappelé",
|
"messageHasBeenRecalled": "Ce message a été rappelé",
|
||||||
@ -357,6 +358,7 @@
|
|||||||
"hotRooms": "Chambres chaudes",
|
"hotRooms": "Chambres chaudes",
|
||||||
"viewMore": "Voir plus",
|
"viewMore": "Voir plus",
|
||||||
"noData": "Aucune donnée",
|
"noData": "Aucune donnée",
|
||||||
|
"noRoomRecordsRecently": "Aucun historique de salle récent.",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"rooms": "Chambres",
|
"rooms": "Chambres",
|
||||||
"coins": "pièces de monnaie",
|
"coins": "pièces de monnaie",
|
||||||
@ -646,6 +648,7 @@
|
|||||||
"purchaseIsSuccessful": "L'achat est réussi",
|
"purchaseIsSuccessful": "L'achat est réussi",
|
||||||
"buy": "Acheter",
|
"buy": "Acheter",
|
||||||
"followed": "Suivi",
|
"followed": "Suivi",
|
||||||
|
"joined": "Rejointes",
|
||||||
"follow2": "Suivre : {1}",
|
"follow2": "Suivre : {1}",
|
||||||
"fans2": "Ventilateurs : {1}",
|
"fans2": "Ventilateurs : {1}",
|
||||||
"vistors2": "Visiteurs :{1}",
|
"vistors2": "Visiteurs :{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "Aucune récompense gagnée. Plus de chance la prochaine fois.",
|
"roomRocketRewardEmptyText": "Aucune récompense gagnée. Plus de chance la prochaine fois.",
|
||||||
"roomRocketRecordEmptyText": "Aucun enregistrement de récompense",
|
"roomRocketRecordEmptyText": "Aucun enregistrement de récompense",
|
||||||
"launchRocket": "Lancement de fusée",
|
"launchRocket": "Lancement de fusée",
|
||||||
"roomRocketFuelFullNotice": "Le carburant de la fusée est plein ! Lancement dans {1} s !"
|
"roomRocketFuelFullNotice": "Le carburant de la fusée est plein ! Lancement dans {1} s !",
|
||||||
|
"leaderboardWealth": "Richesse",
|
||||||
|
"leaderboardCharm": "Charme",
|
||||||
|
"leaderboardRoom": "Salle"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,6 +228,7 @@
|
|||||||
"youDontHaveAnyCouponsYet": "Non hai ancora alcun coupon.",
|
"youDontHaveAnyCouponsYet": "Non hai ancora alcun coupon.",
|
||||||
"recall": "Ricorda",
|
"recall": "Ricorda",
|
||||||
"youHaventFollowed": "Non hai seguito nessuna stanza",
|
"youHaventFollowed": "Non hai seguito nessuna stanza",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "Non ho ancora partecipato a nessuna stanza.",
|
||||||
"deleteFromMyDevice": "Elimina dal mio dispositivo",
|
"deleteFromMyDevice": "Elimina dal mio dispositivo",
|
||||||
"deleteOnAllDevices": "Elimina su tutti i dispositivi",
|
"deleteOnAllDevices": "Elimina su tutti i dispositivi",
|
||||||
"messageHasBeenRecalled": "Questo messaggio è stato richiamato",
|
"messageHasBeenRecalled": "Questo messaggio è stato richiamato",
|
||||||
@ -357,6 +358,7 @@
|
|||||||
"hotRooms": "Camere calde",
|
"hotRooms": "Camere calde",
|
||||||
"viewMore": "Visualizza altro",
|
"viewMore": "Visualizza altro",
|
||||||
"noData": "Nessun dato",
|
"noData": "Nessun dato",
|
||||||
|
"noRoomRecordsRecently": "Nessun record recente delle stanze.",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
"rooms": "Camere",
|
"rooms": "Camere",
|
||||||
"coins": "monete",
|
"coins": "monete",
|
||||||
@ -646,6 +648,7 @@
|
|||||||
"purchaseIsSuccessful": "L'acquisto è andato a buon fine",
|
"purchaseIsSuccessful": "L'acquisto è andato a buon fine",
|
||||||
"buy": "Acquista",
|
"buy": "Acquista",
|
||||||
"followed": "Seguito",
|
"followed": "Seguito",
|
||||||
|
"joined": "Partecipate",
|
||||||
"follow2": "Segui:{1}",
|
"follow2": "Segui:{1}",
|
||||||
"fans2": "Ventole:{1}",
|
"fans2": "Ventole:{1}",
|
||||||
"vistors2": "Visitatori:{1}",
|
"vistors2": "Visitatori:{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "Nessun premio guadagnato. Più fortuna la prossima volta.",
|
"roomRocketRewardEmptyText": "Nessun premio guadagnato. Più fortuna la prossima volta.",
|
||||||
"roomRocketRecordEmptyText": "Nessun record di ricompensa",
|
"roomRocketRecordEmptyText": "Nessun record di ricompensa",
|
||||||
"launchRocket": "Lancia il razzo",
|
"launchRocket": "Lancia il razzo",
|
||||||
"roomRocketFuelFullNotice": "Carburante del razzo pieno! Lancio tra {1} s!"
|
"roomRocketFuelFullNotice": "Carburante del razzo pieno! Lancio tra {1} s!",
|
||||||
|
"leaderboardWealth": "Ricchezza",
|
||||||
|
"leaderboardCharm": "Fascino",
|
||||||
|
"leaderboardRoom": "Stanza"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,6 +228,7 @@
|
|||||||
"youDontHaveAnyCouponsYet": "Você ainda não tem cupons.",
|
"youDontHaveAnyCouponsYet": "Você ainda não tem cupons.",
|
||||||
"recall": "Lembre-se",
|
"recall": "Lembre-se",
|
||||||
"youHaventFollowed": "Você não seguiu nenhuma sala",
|
"youHaventFollowed": "Você não seguiu nenhuma sala",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "Ainda não entrei em nenhuma sala.",
|
||||||
"deleteFromMyDevice": "Excluir do meu dispositivo",
|
"deleteFromMyDevice": "Excluir do meu dispositivo",
|
||||||
"deleteOnAllDevices": "Excluir em todos os dispositivos",
|
"deleteOnAllDevices": "Excluir em todos os dispositivos",
|
||||||
"messageHasBeenRecalled": "Esta mensagem foi recuperada",
|
"messageHasBeenRecalled": "Esta mensagem foi recuperada",
|
||||||
@ -357,6 +358,7 @@
|
|||||||
"hotRooms": "Salas Quentes",
|
"hotRooms": "Salas Quentes",
|
||||||
"viewMore": "Ver mais",
|
"viewMore": "Ver mais",
|
||||||
"noData": "Sem dados",
|
"noData": "Sem dados",
|
||||||
|
"noRoomRecordsRecently": "Nenhum registro recente de salas.",
|
||||||
"users": "Usuários",
|
"users": "Usuários",
|
||||||
"rooms": "Quartos",
|
"rooms": "Quartos",
|
||||||
"coins": "moedas",
|
"coins": "moedas",
|
||||||
@ -646,6 +648,7 @@
|
|||||||
"purchaseIsSuccessful": "A compra foi bem-sucedida",
|
"purchaseIsSuccessful": "A compra foi bem-sucedida",
|
||||||
"buy": "Comprar",
|
"buy": "Comprar",
|
||||||
"followed": "Seguido",
|
"followed": "Seguido",
|
||||||
|
"joined": "Participadas",
|
||||||
"follow2": "Siga: {1}",
|
"follow2": "Siga: {1}",
|
||||||
"fans2": "Fãs: {1}",
|
"fans2": "Fãs: {1}",
|
||||||
"vistors2": "Visitantes:{1}",
|
"vistors2": "Visitantes:{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "Nenhuma recompensa ganha. Melhor sorte da próxima vez.",
|
"roomRocketRewardEmptyText": "Nenhuma recompensa ganha. Melhor sorte da próxima vez.",
|
||||||
"roomRocketRecordEmptyText": "Nenhum registro de recompensa",
|
"roomRocketRecordEmptyText": "Nenhum registro de recompensa",
|
||||||
"launchRocket": "Lançar foguete",
|
"launchRocket": "Lançar foguete",
|
||||||
"roomRocketFuelFullNotice": "Combustível do foguete cheio! Lançamento em {1}s!"
|
"roomRocketFuelFullNotice": "Combustível do foguete cheio! Lançamento em {1}s!",
|
||||||
|
"leaderboardWealth": "Riqueza",
|
||||||
|
"leaderboardCharm": "Charme",
|
||||||
|
"leaderboardRoom": "Sala"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,7 +216,8 @@
|
|||||||
"sendCoupontips": "Bu kuponu bu kullanıcıya göndermek istediğinizden emin misiniz?",
|
"sendCoupontips": "Bu kuponu bu kullanıcıya göndermek istediğinizden emin misiniz?",
|
||||||
"youDontHaveAnyCouponsYet": "Henüz hiç kuponunuz yok.",
|
"youDontHaveAnyCouponsYet": "Henüz hiç kuponunuz yok.",
|
||||||
"recall": "Geri Çağır",
|
"recall": "Geri Çağır",
|
||||||
"youHaventFollowed": "Hiç oda takip etmediniz",
|
"youHaventFollowed": "Hiç oda takip etmediniz",
|
||||||
|
"youHaventJoinedAnyRoomsYet": "Henüz hiçbir odaya katılmadım.",
|
||||||
"deleteFromMyDevice": "Cihazımdan Sil",
|
"deleteFromMyDevice": "Cihazımdan Sil",
|
||||||
"deleteOnAllDevices": "Tüm Cihazlarda Sil",
|
"deleteOnAllDevices": "Tüm Cihazlarda Sil",
|
||||||
"messageHasBeenRecalled": "Bu mesaj geri çekildi",
|
"messageHasBeenRecalled": "Bu mesaj geri çekildi",
|
||||||
@ -355,7 +356,8 @@
|
|||||||
"history": "Tarihçe",
|
"history": "Tarihçe",
|
||||||
"hotRooms": "Popüler Odalar",
|
"hotRooms": "Popüler Odalar",
|
||||||
"viewMore": "Daha Fazlasını Gör",
|
"viewMore": "Daha Fazlasını Gör",
|
||||||
"noData": "Veri yok",
|
"noData": "Veri yok",
|
||||||
|
"noRoomRecordsRecently": "Yakın zamanda oda kaydı yok.",
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
"rooms": "Odalar",
|
"rooms": "Odalar",
|
||||||
"coins": "jetton",
|
"coins": "jetton",
|
||||||
@ -614,7 +616,8 @@
|
|||||||
"mountains": "Araçlar",
|
"mountains": "Araçlar",
|
||||||
"purchaseIsSuccessful": "Satın alma başarılı",
|
"purchaseIsSuccessful": "Satın alma başarılı",
|
||||||
"buy": "Satın Al",
|
"buy": "Satın Al",
|
||||||
"followed": "Takip Edildi",
|
"followed": "Takip Edildi",
|
||||||
|
"joined": "Katıldıklarım",
|
||||||
"follow2": "Takip:{1}",
|
"follow2": "Takip:{1}",
|
||||||
"fans2": "Hayranlar:{1}",
|
"fans2": "Hayranlar:{1}",
|
||||||
"vistors2": "Ziyaretçiler:{1}",
|
"vistors2": "Ziyaretçiler:{1}",
|
||||||
@ -748,5 +751,8 @@
|
|||||||
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
|
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
|
||||||
"roomRocketRecordEmptyText": "Ödül kaydı yok",
|
"roomRocketRecordEmptyText": "Ödül kaydı yok",
|
||||||
"launchRocket": "Roketi Fırlat",
|
"launchRocket": "Roketi Fırlat",
|
||||||
"roomRocketFuelFullNotice": "Roket yakıtı doldu! {1} saniye sonra fırlatılıyor!"
|
"roomRocketFuelFullNotice": "Roket yakıtı doldu! {1} saniye sonra fırlatılıyor!",
|
||||||
|
"leaderboardWealth": "Servet",
|
||||||
|
"leaderboardCharm": "Cazibe",
|
||||||
|
"leaderboardRoom": "Oda"
|
||||||
}
|
}
|
||||||
|
|||||||
38
design-qa.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
**Design QA**
|
||||||
|
|
||||||
|
- Source visual truth: Figma `3351:5626` and `/var/folders/pb/pgzxbnfd2jd8nmtbykd31h2h0000gn/T/codex-clipboard-5ea92794-9b5a-469e-bf5c-7eec4b63206d.png`
|
||||||
|
- Implementation screenshots: `/tmp/yumi_party_after_closed.png` and `/tmp/yumi_tab_after_wait.png`
|
||||||
|
- Combined comparison: `/tmp/yumi_party_design_comparison.png`
|
||||||
|
- Viewport: Pixel 6a, 1080 x 2400, Android 16
|
||||||
|
- State: logged-in Home/Party and Me screens with live API data
|
||||||
|
|
||||||
|
**Full-view comparison evidence**
|
||||||
|
|
||||||
|
- The supplied 375 x 727 moon background begins at the top of the app content and continues to the bottom navigation on Home and Me.
|
||||||
|
- Wealth uses the 172 x 172 transparent treasure artwork clipped inside the 172 x 130 card instead of stretching it as a full background.
|
||||||
|
- Charm and Room use 66 x 66 artwork at the Figma offsets.
|
||||||
|
- All leaderboard avatars are circular cover crops and retain a visible gold, silver, or bronze frame.
|
||||||
|
|
||||||
|
**Focused region comparison evidence**
|
||||||
|
|
||||||
|
- Compared the 355 x 130 leaderboard region in Figma with the same region from the Pixel 6a screenshot.
|
||||||
|
- Dynamic banner, avatar, country, and room API content differs from Figma sample content as expected; layout geometry and image treatment match the requested surfaces.
|
||||||
|
|
||||||
|
**Findings**
|
||||||
|
|
||||||
|
- No actionable P0, P1, or P2 mismatch remains for the requested background, leaderboard artwork placement, avatar cover mode, or avatar frames.
|
||||||
|
- P3: live banner and room content cannot be pixel-identical to Figma placeholder content.
|
||||||
|
|
||||||
|
**Comparison history**
|
||||||
|
|
||||||
|
- Initial: square decoration assets were stretched or mispositioned, and leaderboard avatars had no rank frames.
|
||||||
|
- Fix: applied Figma layer dimensions and offsets, added frame assets, and forced static and GIF avatars to `BoxFit.cover`.
|
||||||
|
- Post-fix: the combined comparison and device screenshots show correct artwork placement, framed avatars, and no visible overflow.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `flutter analyze --no-fatal-infos --no-fatal-warnings` completed with exit code 0 for the six affected Dart files.
|
||||||
|
- Android debug APK built, installed, and launched successfully on the Pixel 6a.
|
||||||
|
- No load failure from the new local background/rank-frame assets and no RenderFlex overflow was observed. An existing empty network-image exception was emitted while navigating outside the leaderboard flow; it is not produced by the guarded leaderboard avatar builder.
|
||||||
|
|
||||||
|
final result: passed
|
||||||
456
docs/后端接口对接规范.md
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
# Yumi Flutter 后端接口对接规范
|
||||||
|
|
||||||
|
> 适用仓库:`yumi-flutter`
|
||||||
|
> 后端代码:`yumi-java` + `yumi-golang`
|
||||||
|
> 最后核对:2026-07-10
|
||||||
|
|
||||||
|
## 1. 目标与适用范围
|
||||||
|
|
||||||
|
本文档用于统一 Yumi Flutter 与 Java、Go 后端的接口对接方式,覆盖接口事实来源、路由归属、前端分层、响应解析、鉴权、错误处理、验证和问题流转。
|
||||||
|
|
||||||
|
核心原则:
|
||||||
|
|
||||||
|
- 先查代码和配置,再实现;禁止根据页面或旧模型猜测后端契约。
|
||||||
|
- Flutter 线程对两个后端仓库仅有只读权限,发现后端问题只记录证据和建议。
|
||||||
|
- 所有业务 HTTP 请求必须经过项目现有网络层,页面不直接解析原始响应。
|
||||||
|
- 接口失败时禁止使用本地假数据、硬编码列表或伪造成功结果还原页面。
|
||||||
|
- Bug 修复必须先说明 Root Cause 和影响面,经确认后再做最小改动并提供验证证据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 仓库位置与责任边界
|
||||||
|
|
||||||
|
### 2.1 Flutter 客户端
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/pinpaibu/flutter_code/yumi-flutter
|
||||||
|
```
|
||||||
|
|
||||||
|
当前技术和分层事实:
|
||||||
|
|
||||||
|
- 网络:Dio,统一客户端为 `lib/shared/data_sources/sources/remote/net/network_client.dart`。
|
||||||
|
- 响应封装:`ResponseData<T>`。
|
||||||
|
- 仓储接口:`lib/shared/business_logic/repositories/`。
|
||||||
|
- 仓储实现:`lib/shared/data_sources/sources/repositories/`。
|
||||||
|
- 请求/响应模型:`lib/shared/business_logic/models/req/` 与 `res/`。
|
||||||
|
- 功能模块:`lib/modules/`;独立功能也可在模块内保留 `data/api/repository/models` 边界,例如 `lib/modules/room_game/data/`。
|
||||||
|
- 页面状态:Provider + ChangeNotifier,全局管理器在 `lib/services/`。
|
||||||
|
- 路由:Fluro,入口为 `lib/main.dart`。
|
||||||
|
|
||||||
|
### 2.2 Java 后端
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/pinpaibu/server_code/yumi-java
|
||||||
|
```
|
||||||
|
|
||||||
|
客户端相关的主要边界:
|
||||||
|
|
||||||
|
- `rc-gateway/`:Spring Cloud Gateway,是 Flutter 默认 HTTP 入口。
|
||||||
|
- `nacos_config/rc-gateway/application.yml`:外部路径到 Java/Go 服务的路由事实。
|
||||||
|
- `rc-auth/`:登录凭证和内部 Token 校验。
|
||||||
|
- `rc-service/rc-service-other/`:用户、配置、礼物、任务、房间外围等大量 App 业务。
|
||||||
|
- `rc-service/rc-service-live/`:语音房在线、麦位等实时业务。
|
||||||
|
- `rc-service/rc-service-wallet/`:钱包、余额、流水。
|
||||||
|
- `rc-service/rc-service-order/`:订单与支付。
|
||||||
|
- `rc-service/rc-service-external/`:文件、短信、IM/TRTC 等外部能力。
|
||||||
|
- `rc-service/rc-inner-api/`:Java 服务内部 Feign 契约,Flutter 不直接调用。
|
||||||
|
|
||||||
|
Java `README.md` 可用于快速定位模块,但其端口和模块说明可能滞后;联调时必须以 Gateway/Nacos 路由和当前 Controller 为准。
|
||||||
|
|
||||||
|
### 2.3 Go 后端
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/pinpaibu/server_code/yumi-golang
|
||||||
|
```
|
||||||
|
|
||||||
|
当前主要边界:
|
||||||
|
|
||||||
|
- `cmd/api/`:HTTP API 启动和依赖装配。
|
||||||
|
- `cmd/consumer/`:事件消费入口。
|
||||||
|
- `internal/router/`:Gin 路由、请求绑定、鉴权中间件和统一响应。
|
||||||
|
- `internal/service/<module>/`:按业务模块拆分的业务逻辑。
|
||||||
|
- `internal/integration/`:Go 调用 Java 服务的网关封装。
|
||||||
|
- `internal/common/`:`AppError`、`AuthUser` 等跨模块类型。
|
||||||
|
- `internal/model/` 与 `migrations/`:表映射和数据库迁移。
|
||||||
|
- `internal/config/config.go`:运行配置与 Java 下游地址。
|
||||||
|
|
||||||
|
Go 本地 HTTP 默认监听 `:2900`,但 Flutter 默认仍应通过 Java Gateway 访问,不应在业务代码中硬编码内部端口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 后端仓库只读红线
|
||||||
|
|
||||||
|
在 Flutter 对接任务中,`yumi-java` 和 `yumi-golang` 一律视为只读证据源。
|
||||||
|
|
||||||
|
允许:
|
||||||
|
|
||||||
|
- 使用 `rg`、`find`、`sed -n`、`git status --short`、`git diff`、`git show` 查看和搜索。
|
||||||
|
- 读取路由、Controller/Handler、DTO、Service、错误码、配置和测试。
|
||||||
|
- 在 Flutter 文档中记录后端证据路径和待确认事项。
|
||||||
|
|
||||||
|
禁止:
|
||||||
|
|
||||||
|
- 在后端仓库创建、修改、删除、移动或格式化任何代码、文档、配置、SQL、生成文件和测试。
|
||||||
|
- 运行会产生文件的命令,如 `gofmt -w`、`goimports -w`、`go mod tidy`、Maven 打包、代码生成、数据库迁移。
|
||||||
|
- 默认启动后端、Docker、消费者、定时任务或执行后端测试;确有必要时必须先说明作用和文件/数据影响,并得到用户明确授权。
|
||||||
|
- 在后端仓库执行暂存、提交、切分支、合并、拉取或推送。
|
||||||
|
- 把调试标记或前端待办写入后端源码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 实际请求拓扑与路由归属
|
||||||
|
|
||||||
|
### 4.1 默认链路
|
||||||
|
|
||||||
|
```text
|
||||||
|
Flutter
|
||||||
|
-> Java Gateway(API_HOST)
|
||||||
|
-> Java auth / external / order / wallet / live / other
|
||||||
|
-> Go API
|
||||||
|
-> MySQL / Redis / MQ
|
||||||
|
-> Java 内部服务(鉴权、用户、钱包、奖励等)
|
||||||
|
```
|
||||||
|
|
||||||
|
Flutter 默认基地址由 `SCVariant1Config.apiHost` 提供,可通过编译参数覆盖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run --dart-define=API_HOST=https://example-gateway/
|
||||||
|
```
|
||||||
|
|
||||||
|
不得在 Repository、Manager 或 Widget 中硬编码生产/测试域名。
|
||||||
|
|
||||||
|
### 4.2 Java Gateway 路由要点
|
||||||
|
|
||||||
|
以 `yumi-java/nacos_config/rc-gateway/application.yml` 为准,当前关键对外路径如下:
|
||||||
|
|
||||||
|
| Flutter 请求路径 | Gateway 处理 | 实际归属 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/auth/**` | 去掉 `/auth` | Java auth |
|
||||||
|
| `/external/**` | 去掉 `/external` | Java external |
|
||||||
|
| `/order/**` | 去掉 `/order` | Java order |
|
||||||
|
| `/wallet/**` | 去掉 `/wallet` | Java wallet |
|
||||||
|
| `/live/**` | 去掉 `/live` | Java live |
|
||||||
|
| `/go/**` | 去掉 `/go` | Go API |
|
||||||
|
| `/app/game/**` | 保留原路径 | Go API |
|
||||||
|
| 未匹配的普通路径 | 默认转发 | Java other |
|
||||||
|
| 带 `Req-Likei: true` 的加密路由 | `ForwardRoute` 解析后转发 | 由解析结果决定 |
|
||||||
|
|
||||||
|
Gateway 还对部分活动、回调和后台路径做了专用路由;对接具体接口时必须搜索完整 path,不能只根据前缀推断服务归属。
|
||||||
|
|
||||||
|
### 4.3 Go 路径的两种对接形式
|
||||||
|
|
||||||
|
- 经 Gateway 的 `/go` 路由:Flutter 请求 `/go/app/vip/status`,Go 实际接收 `/app/vip/status`。
|
||||||
|
- Gateway 专用路由:例如 Flutter 请求 `/app/game/providers`,Go 仍接收 `/app/game/providers`。
|
||||||
|
|
||||||
|
除已有调试边界外,新增 App 接口优先使用同一 `API_HOST` 经 Gateway 访问。项目已有两个可选直连覆盖:
|
||||||
|
|
||||||
|
- `VIP_API_HOST`:VIP Go API 调试。
|
||||||
|
- `GAME_API_HOST`:房间游戏 Go API 调试。
|
||||||
|
|
||||||
|
仅在确认需要绕过 Gateway 联调时使用;不得为单个页面随意新增直连域名。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 接口事实参考顺序
|
||||||
|
|
||||||
|
当前两个后端仓库没有可覆盖全部 App 接口的统一 OpenAPI 契约,因此必须以可执行代码和当前路由配置为主证据。
|
||||||
|
|
||||||
|
### 5.1 先确定请求归属
|
||||||
|
|
||||||
|
1. 在 `nacos_config/rc-gateway/application.yml` 搜索完整 path 或前缀。
|
||||||
|
2. 判断请求是否会 `StripPrefix`,记录 Flutter path 与下游 path。
|
||||||
|
3. 确认该路由是否在 `gateway.ignorePaths` 中;“忽略 Gateway 鉴权”不等于“业务无需鉴权”,还要继续检查下游实现。
|
||||||
|
|
||||||
|
### 5.2 Java 接口
|
||||||
|
|
||||||
|
按以下顺序核对:
|
||||||
|
|
||||||
|
1. `*-adapter/src/main/java/**/**RestController.java`:`@RequestMapping` + `@Get/Post/Put/DeleteMapping` 确认 method/path。
|
||||||
|
2. Controller 方法参数:确认 query/path/form/json、`@Validated` 和必填规则。
|
||||||
|
3. `*-client` / `*-inner` 中的 Command、CO、DTO、Enum:确认字段名、类型、枚举和响应数据。
|
||||||
|
4. `*-application` 中对应 Service:确认业务条件、空值和错误分支。
|
||||||
|
5. 错误码枚举、`ApiException` 和统一响应框架:确认业务失败语义。
|
||||||
|
6. 最后查看相关测试和 README/docs,用于补充流程,不用于覆盖当前代码。
|
||||||
|
|
||||||
|
### 5.3 Go 接口
|
||||||
|
|
||||||
|
按以下顺序核对:
|
||||||
|
|
||||||
|
1. `internal/router/<module>_routes.go`:确认 method/path、鉴权中间件、query/path/body 绑定。
|
||||||
|
2. `internal/service/<module>/types.go` 及对应业务文件:确认请求/响应字段、默认值、业务规则和 `AppError`。
|
||||||
|
3. `internal/integration/`:如业务依赖 Java,确认实际下游 endpoint、Header 和数值转换。
|
||||||
|
4. `internal/model/`、`migrations/` 和 `internal/config/`:仅用于理解字段来源、持久化和运行限制。
|
||||||
|
5. `*_test.go`:核对边界值和已固化行为。
|
||||||
|
|
||||||
|
### 5.4 证据冲突时
|
||||||
|
|
||||||
|
- Gateway 配置决定外部请求实际到达哪个服务。
|
||||||
|
- 当前 Router/Controller 实现决定 method、path 和参数接收方式。
|
||||||
|
- 当前 Service/DTO 实现决定业务语义和字段。
|
||||||
|
- README、注释、历史文档和 Flutter 旧模型只是补充证据。
|
||||||
|
- 冲突时以当前可到达的执行代码作为“现状事实”,同时记录“待后端确认”,Flutter 不自行改后端消除差异。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 已确认的网络契约
|
||||||
|
|
||||||
|
### 6.1 请求 Header
|
||||||
|
|
||||||
|
`ApiInterceptor` 会统一补充:
|
||||||
|
|
||||||
|
- `Authorization: Bearer <token>`(本地存在 Token 时)
|
||||||
|
- `req-lang`
|
||||||
|
- `req-imei`
|
||||||
|
- `req-app-intel`
|
||||||
|
- `req-client`
|
||||||
|
- `req-sys-origin`
|
||||||
|
- `Host`
|
||||||
|
- 已确认的加密路由才会添加 `Req-Likei: true`
|
||||||
|
|
||||||
|
对接时不得在 Widget/Manager 重复手工组装这些公共 Header,也不得在代码或日志中写死 Token、密钥或完整鉴权数据。
|
||||||
|
|
||||||
|
Go App 路由的 `authMiddleware` 会读取 Bearer Token,并调用 Java auth 校验凭证;因此 Go 接口鉴权仍与 Java 登录态一致。
|
||||||
|
|
||||||
|
### 6.2 Java/Go 兼容响应
|
||||||
|
|
||||||
|
Flutter `ResponseData<T>` 当前兼容两类字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": true,
|
||||||
|
"errorCode": 0,
|
||||||
|
"errorCodeName": "",
|
||||||
|
"errorMsg": "success",
|
||||||
|
"body": {},
|
||||||
|
"time": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
以及:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "OK",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Go `writeOK` / `writeError` 会同时输出 `status/errorCode/errorMsg/body` 与 `code/message/data` 两套兼容字段。Flutter 解析规则为:
|
||||||
|
|
||||||
|
- 数据优先读取 `body`,不存在时读取 `data`。
|
||||||
|
- 优先用 `status` 判定成败;缺失时,`code == 0` 或 `code == 200` 视为成功。
|
||||||
|
- 错误码读取 `errorCode` 或 `code`,错误信息读取 `errorMsg` 或 `message`。
|
||||||
|
- HTTP 200 不代表业务成功,仍必须检查 envelope。
|
||||||
|
- HTTP 4xx/5xx 与业务失败都必须进入明确错误状态。
|
||||||
|
|
||||||
|
新接口不得在页面层再实现一套 envelope 判定逻辑。
|
||||||
|
|
||||||
|
### 6.3 空响应
|
||||||
|
|
||||||
|
- 只有后端契约明确表示成功无数据时,才允许使用可空泛型和 `allowNullBody: true`。
|
||||||
|
- 不得用 `false`、`0`、空字符串或空列表掩盖本应返回对象的契约问题。
|
||||||
|
- 列表本身合法为空时,空列表是真实业务结果,不是接口失败的替代值。
|
||||||
|
|
||||||
|
### 6.4 路径与加密路由
|
||||||
|
|
||||||
|
- 旧接口在 Repository 中仍可能使用哈希形式的路由键,`network_client.dart` 会用 `_localRouteOverrides` 转为已知明文 path。
|
||||||
|
- 新对接优先使用后端路由中已确认的明文 path,不复制或自行生成哈希。
|
||||||
|
- 若线上 Gateway 明确要求加密路由,必须先获得后端路由键/转发规则并在网络层集中处理,页面层不得设置 `Req-Likei`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Flutter 对接落地规范
|
||||||
|
|
||||||
|
### 7.1 分层责任
|
||||||
|
|
||||||
|
```text
|
||||||
|
Widget / Page
|
||||||
|
-> Provider / ChangeNotifier / Manager
|
||||||
|
-> Repository abstraction
|
||||||
|
-> Repository implementation / feature Api
|
||||||
|
-> NetworkClient
|
||||||
|
-> Java Gateway -> Java or Go
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Widget / Page`:只负责渲染、交互与发出意图;不直接调 Dio,不解析 JSON。
|
||||||
|
- `Provider / ChangeNotifier / Manager`:编排加载、成功、空态和错误状态,调用 Repository,不处理原始 envelope。
|
||||||
|
- `Repository abstraction`:定义业务语义明确的方法和返回类型。
|
||||||
|
- `Repository implementation / feature Api`:定义 method/path/query/body,调用 `http.get/post/put/delete`,完成 model 映射。
|
||||||
|
- `NetworkClient`:统一 base URL、Header、Token、超时、envelope 和通用错误处理。
|
||||||
|
|
||||||
|
对现有模块增加接口时,优先延续该模块的已有分层,不为单个接口引入第二套网络或状态管理方案。
|
||||||
|
|
||||||
|
### 7.2 Model 与字段
|
||||||
|
|
||||||
|
- 字段命名、类型、可空性、枚举值和时间单位必须有后端证据。
|
||||||
|
- 后端 `int64/Long` 字段如可能以数字或数字字符串返回,只能在 Model 解析边界做兼容,不得在 Widget 中反复转换。
|
||||||
|
- 时间字段必须明确是秒、毫秒还是 ISO-8601,禁止通过数值长度猜测。
|
||||||
|
- 金额、金币和比例必须确认单位与精度,不得在 UI 层暗中乘除。
|
||||||
|
- 枚举未知值应有安全的“未知/不支持”分支,但不得把未知值伪装成某个已知业务类型。
|
||||||
|
- 不在 Model 中根据页面需求虚构后端未返回的业务字段。
|
||||||
|
|
||||||
|
### 7.3 请求参数
|
||||||
|
|
||||||
|
- GET 查询使用 `queryParams`,path variable 只用已确认的 URL 片段。
|
||||||
|
- JSON 请求使用 `data`,参数命名与后端契约一致。
|
||||||
|
- 文件上传使用 `FormData`/`MultipartFile`,必须同时确认表单字段名、MIME、扩展名、大小上限和超时。
|
||||||
|
- 幂等写操作的 key 由调用方在一次用户意图开始时创建,同一次超时重试必须复用;不得在每次底层请求时重新生成。
|
||||||
|
- 分页必须记录是 cursor、page/size 还是 lastId,不得混用。
|
||||||
|
|
||||||
|
### 7.4 错误和登录态
|
||||||
|
|
||||||
|
- 网络层负责超时、HTTP 错误、通用业务错误和当前登录态校验。
|
||||||
|
- Manager/Provider 只负责将错误转为页面可表达的错误状态,必要时提供重试入口。
|
||||||
|
- Widget 不得看 `errorCode` 决定登出,不得自行重试鉴权失败。
|
||||||
|
- 不得把服务端内部堆栈、SQL、密钥、完整 Token 或敏感请求体输出到生产日志。
|
||||||
|
- 同一页面需要部分容错时,必须先定义每个数据源的独立成功/失败语义,不能用本地假数据补齐失败部分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 严禁本地数据兜底
|
||||||
|
|
||||||
|
接口错误、超时、解析失败或返回不完整时,禁止:
|
||||||
|
|
||||||
|
- 返回内置 mock 列表、本地 JSON、调试 assets 或硬编码对象。
|
||||||
|
- 把异常转换为空列表、`false`、`0` 或“成功”,使页面误以为接口已成功。
|
||||||
|
- 因为新接口不可用而静默切换到语义不同的旧接口。
|
||||||
|
- 用占位数据触发下游支付、送礼、发奖、扣费、房间状态或其他写操作。
|
||||||
|
|
||||||
|
允许的仅是产品明确设计的本地能力,例如:
|
||||||
|
|
||||||
|
- 加载骨架、空态插图、错误提示和重试按钮,但不得表示为服务端真实数据。
|
||||||
|
- 明确定义的离线缓存,但必须保留“旧数据/离线”语义,不能覆盖本次请求失败事实。
|
||||||
|
- 纯 UI 预览和单元测试中的 Fake/Fixture,必须与生产请求路径隔离。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 标准对接流程
|
||||||
|
|
||||||
|
1. 明确本次业务范围,不顺手改无关模块。
|
||||||
|
2. 在 Flutter 中搜索现有接口、Model、Manager 和测试,确认已有状态和兼容要求。
|
||||||
|
3. 查 Java Gateway 路由,确定请求归属和外部 path。
|
||||||
|
4. 按第 5 节读取 Java 或 Go 的 Router/Controller、DTO/Service、错误和测试。
|
||||||
|
5. 先写一份接口事实记录,至少包含 method/path/auth/request/response/error/evidence。
|
||||||
|
6. 说明改动对现有路由、网络层、登录态、模型和页面的影响;若为 Bug,先提供 Root Cause 并等待用户确认。
|
||||||
|
7. 在 Flutter 做能解决问题的最小变更,不重构无关代码。
|
||||||
|
8. 增加 Model/envelope/分页/错误分支的针对性测试。
|
||||||
|
9. 按第 11 节执行验证,并记录真实 API 联调结果。
|
||||||
|
10. 如后端需要调整,只记录待确认项和证据,由用户协调后端处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 接口事实记录模板
|
||||||
|
|
||||||
|
```text
|
||||||
|
### 接口名称
|
||||||
|
|
||||||
|
- 业务模块:
|
||||||
|
- 调用链路:Flutter -> Java Gateway -> Java / Go -> 下游
|
||||||
|
- Flutter Method / Path:
|
||||||
|
- 下游 Method / Path:
|
||||||
|
- Gateway 路由处理:保留路径 / StripPrefix / 加密转发
|
||||||
|
- Auth:无 / Bearer / 其他
|
||||||
|
- Request:
|
||||||
|
- path:
|
||||||
|
- query:
|
||||||
|
- header:
|
||||||
|
- body/form:
|
||||||
|
- Response body/data:
|
||||||
|
- 空值与默认值:
|
||||||
|
- 业务错误码:
|
||||||
|
- 时间/金额/分页/幂等语义:
|
||||||
|
- 后端证据:
|
||||||
|
- Gateway:
|
||||||
|
- Router / Controller:
|
||||||
|
- DTO / Service:
|
||||||
|
- Test / Doc:
|
||||||
|
- Flutter 落地:
|
||||||
|
- repository/api:
|
||||||
|
- model:
|
||||||
|
- manager/provider:
|
||||||
|
- view:
|
||||||
|
- test:
|
||||||
|
- 待后端确认:
|
||||||
|
- 当前状态:未对接 / 对接中 / 已对接 / 等后端
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 验证规范
|
||||||
|
|
||||||
|
### 11.1 静态与自动化验证
|
||||||
|
|
||||||
|
只格式化本次改动的 Dart 文件,然后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter analyze
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
网络 envelope 或通用解析改动至少运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter test test/network_response_data_test.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
业务 Model 改动应运行或新增对应 `*_test.dart`;不能只依赖 Widget 手工观察。
|
||||||
|
|
||||||
|
### 11.2 真实接口联调
|
||||||
|
|
||||||
|
至少验证:
|
||||||
|
|
||||||
|
1. 实际请求 method/path 与 Gateway 转发后 path 正确。
|
||||||
|
2. 需鉴权接口携带当前 Bearer Token,无 Token/失效 Token 进入错误态。
|
||||||
|
3. 成功、真实空数据、业务失败、HTTP 失败、超时、解析失败各至少覆盖相关分支。
|
||||||
|
4. 重复点击、翻页、刷新和页面销毁不会产生重复写入或过期状态覆盖。
|
||||||
|
5. 接口失败时页面不出现本地假数据,也不把失败当作空数据。
|
||||||
|
6. 错误信息可理解、可重试,且日志不包含敏感信息。
|
||||||
|
|
||||||
|
### 11.3 回归影响
|
||||||
|
|
||||||
|
每次交付要明确说明:
|
||||||
|
|
||||||
|
- 是否修改公共 `NetworkClient`、`ResponseData`、公共 Header 或 Token 处理。
|
||||||
|
- 是否影响旧哈希路由、明文路由或 base URL override。
|
||||||
|
- 是否影响其他共用 Model、Repository、Provider 或实时消息。
|
||||||
|
- 针对上述影响执行了哪些测试,结果是什么。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 后端问题流转
|
||||||
|
|
||||||
|
出现以下情况时,Flutter 不修改后端:
|
||||||
|
|
||||||
|
- Gateway 路由与 Router/Controller 不一致。
|
||||||
|
- Java/Go 响应字段、可空性、数值类型或枚举不一致。
|
||||||
|
- HTTP 状态码、`errorCode/code` 和业务语义不能支持前端分支。
|
||||||
|
- 鉴权、幂等、分页、排序、时区、时间单位或金额单位不清晰。
|
||||||
|
- Go 路由存在但 Java Gateway 没有可到达转发。
|
||||||
|
- 后端仅有占位实现、返回 mock,或所需下游能力未上线。
|
||||||
|
|
||||||
|
记录模板:
|
||||||
|
|
||||||
|
```text
|
||||||
|
- 【待后端确认|业务模块|YYYY-MM-DD HH:mm CST】
|
||||||
|
问题:
|
||||||
|
现象:
|
||||||
|
证据:
|
||||||
|
- Gateway:
|
||||||
|
- Java / Go:
|
||||||
|
- Flutter:
|
||||||
|
Root Cause 判断:
|
||||||
|
前端影响:
|
||||||
|
建议后端处理:
|
||||||
|
联调验收条件:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 文档维护要求
|
||||||
|
|
||||||
|
- 本文档记录稳定规则;具体接口事实应写入对应业务文档,避免把本文档变成无法维护的接口大全。
|
||||||
|
- 引用后端证据时写明仓库和相对路径,必要时补充类/函数名,不复制大段实现。
|
||||||
|
- 不在文档中记录真实 Token、密码、密钥、DSN、云服务凭证或内网敏感信息。
|
||||||
|
- 后端路由或响应契约变更后,同步更新相关业务接口记录和验证日期。
|
||||||
|
- 文档中的“已对接”必须同时满足:Flutter 代码已落地、自动化验证通过、真实接口联调通过、不存在本地假数据兜底。
|
||||||
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
|||||||
@override
|
@override
|
||||||
String get apiHost => const String.fromEnvironment(
|
String get apiHost => const String.fromEnvironment(
|
||||||
'API_HOST',
|
'API_HOST',
|
||||||
defaultValue: 'http://192.168.110.64:1100/',
|
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||||
); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/”
|
); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/”
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -4,6 +4,7 @@ class SCRoomMsgType {
|
|||||||
static const String joinRoom = "JOIN_ROOM";
|
static const String joinRoom = "JOIN_ROOM";
|
||||||
static const String systemTips = "SYSTEM_TIPS";
|
static const String systemTips = "SYSTEM_TIPS";
|
||||||
static const String cpSystemNotice = "CP_SYSTEM_NOTICE";
|
static const String cpSystemNotice = "CP_SYSTEM_NOTICE";
|
||||||
|
static const String cpRelationBroadcast = "CP_RELATION_BROADCAST";
|
||||||
|
|
||||||
///表情包
|
///表情包
|
||||||
static const String emoticons = "EMOTICONS";
|
static const String emoticons = "EMOTICONS";
|
||||||
|
|||||||
@ -1195,6 +1195,12 @@ class SCAppLocalizations {
|
|||||||
|
|
||||||
String get charm => translate('charm');
|
String get charm => translate('charm');
|
||||||
|
|
||||||
|
String get leaderboardWealth => translate('leaderboardWealth');
|
||||||
|
|
||||||
|
String get leaderboardCharm => translate('leaderboardCharm');
|
||||||
|
|
||||||
|
String get leaderboardRoom => translate('leaderboardRoom');
|
||||||
|
|
||||||
String get specialEffectsManagement => translate('specialEffectsManagement');
|
String get specialEffectsManagement => translate('specialEffectsManagement');
|
||||||
|
|
||||||
String get giftCounter => translate('giftCounter');
|
String get giftCounter => translate('giftCounter');
|
||||||
@ -1533,6 +1539,8 @@ class SCAppLocalizations {
|
|||||||
|
|
||||||
String get followed => translate('followed');
|
String get followed => translate('followed');
|
||||||
|
|
||||||
|
String get joined => translate('joined');
|
||||||
|
|
||||||
String get following => translate('following');
|
String get following => translate('following');
|
||||||
|
|
||||||
String get history => translate('history');
|
String get history => translate('history');
|
||||||
@ -1543,6 +1551,11 @@ class SCAppLocalizations {
|
|||||||
|
|
||||||
String get noData => translate('noData');
|
String get noData => translate('noData');
|
||||||
|
|
||||||
|
String get noRoomRecordsRecently => translate('noRoomRecordsRecently');
|
||||||
|
|
||||||
|
String get youHaventJoinedAnyRoomsYet =>
|
||||||
|
translate('youHaventJoinedAnyRoomsYet');
|
||||||
|
|
||||||
String get freePrice => translate('freePrice');
|
String get freePrice => translate('freePrice');
|
||||||
|
|
||||||
String get purchase => translate('purchase');
|
String get purchase => translate('purchase');
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import 'shared/tools/sc_deep_link_handler.dart';
|
|||||||
import 'shared/tools/sc_deviceId_utils.dart';
|
import 'shared/tools/sc_deviceId_utils.dart';
|
||||||
import 'shared/tools/sc_gift_vap_svga_manager.dart';
|
import 'shared/tools/sc_gift_vap_svga_manager.dart';
|
||||||
import 'shared/data_sources/sources/local/user_manager.dart';
|
import 'shared/data_sources/sources/local/user_manager.dart';
|
||||||
|
import 'shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||||
import 'modules/splash/splash_page.dart';
|
import 'modules/splash/splash_page.dart';
|
||||||
import 'services/general/sc_app_general_manager.dart';
|
import 'services/general/sc_app_general_manager.dart';
|
||||||
import 'services/payment/apple_payment_manager.dart';
|
import 'services/payment/apple_payment_manager.dart';
|
||||||
@ -279,6 +280,8 @@ final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|||||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||||
final RouteObserver<ModalRoute<dynamic>> modalRouteObserver =
|
final RouteObserver<ModalRoute<dynamic>> modalRouteObserver =
|
||||||
RouteObserver<ModalRoute<dynamic>>();
|
RouteObserver<ModalRoute<dynamic>>();
|
||||||
|
final NavigatorObserver floatingScreenRouteObserver =
|
||||||
|
SCFloatingScreenRouteObserver();
|
||||||
|
|
||||||
class YumiApplication extends StatefulWidget {
|
class YumiApplication extends StatefulWidget {
|
||||||
const YumiApplication({super.key});
|
const YumiApplication({super.key});
|
||||||
@ -451,7 +454,11 @@ class _YumiApplicationState extends State<YumiApplication>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
navigatorObservers: [routeObserver, modalRouteObserver],
|
navigatorObservers: [
|
||||||
|
routeObserver,
|
||||||
|
modalRouteObserver,
|
||||||
|
floatingScreenRouteObserver,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import 'package:yumi/shared/tools/sc_google_auth_utils.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.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/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_auth_type.dart';
|
import 'package:yumi/shared/data_sources/models/enum/sc_auth_type.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
@ -56,9 +57,35 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
|
|||||||
);
|
);
|
||||||
Provider.of<SCAppGeneralManager>(context, listen: false).selectCountryInfo =
|
Provider.of<SCAppGeneralManager>(context, listen: false).selectCountryInfo =
|
||||||
null;
|
null;
|
||||||
|
_prefillGoogleRegistrationProfile();
|
||||||
_fillInvitationCodeFromInstallReferrer();
|
_fillInvitationCodeFromInstallReferrer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _prefillGoogleRegistrationProfile() {
|
||||||
|
final authProvider = Provider.of<SocialChatAuthenticationManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
if (authProvider.authType != SCAuthType.GOOGLE.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
birthdayDate = DateTime(2000, 1, 1);
|
||||||
|
final googleUser = authProvider.user ?? SCGoogleAuthService.currentUser;
|
||||||
|
final googleName = googleUser?.displayName?.trim() ?? "";
|
||||||
|
final googleAvatar = googleUser?.photoURL?.trim() ?? "";
|
||||||
|
if (googleName.isNotEmpty) {
|
||||||
|
nicknameController.text = googleName;
|
||||||
|
}
|
||||||
|
if (googleAvatar.isNotEmpty) {
|
||||||
|
final editUser = userProvider?.editUser ?? SCUserProfileCmd();
|
||||||
|
if ((editUser.userAvatar ?? "").trim().isEmpty) {
|
||||||
|
editUser.setUserAvatar = googleAvatar;
|
||||||
|
}
|
||||||
|
userProvider?.editUser = editUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
nicknameController.dispose();
|
nicknameController.dispose();
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
|
||||||
@ -15,6 +14,7 @@ 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/local/user_manager.dart';
|
||||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/msg/message_conversation_list_page.dart';
|
import 'package:yumi/ui_kit/widgets/msg/message_conversation_list_page.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
|
|
||||||
import '../chat_route.dart';
|
import '../chat_route.dart';
|
||||||
|
|
||||||
@ -94,101 +94,29 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
widget.isFromRoom
|
const Positioned.fill(child: SCHomeShellBackground()),
|
||||||
? Image.asset(
|
if (widget.isFromRoom)
|
||||||
_strategy.getMessagePageIndexMaskIcon(),
|
Image.asset(
|
||||||
width: ScreenUtil().screenWidth,
|
_strategy.getMessagePageIndexMaskIcon(),
|
||||||
fit: BoxFit.cover,
|
width: ScreenUtil().screenWidth,
|
||||||
)
|
fit: BoxFit.cover,
|
||||||
: Container(),
|
),
|
||||||
DefaultTabController(
|
Scaffold(
|
||||||
length: 1,
|
backgroundColor: Colors.transparent,
|
||||||
child: Scaffold(
|
body: SafeArea(
|
||||||
backgroundColor: Colors.transparent,
|
bottom: false,
|
||||||
appBar: AppBar(
|
child: Column(
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
leading: null,
|
|
||||||
title:
|
|
||||||
_isSearchingMessages
|
|
||||||
? _messageSearchField()
|
|
||||||
: TabBar(
|
|
||||||
tabs:
|
|
||||||
[SCAppLocalizations.of(context)!.message]
|
|
||||||
.map(
|
|
||||||
(e) => Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: 3.w,
|
|
||||||
top: 4.w,
|
|
||||||
),
|
|
||||||
child: Text(e),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
labelStyle: TextStyle(
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
unselectedLabelStyle: TextStyle(
|
|
||||||
fontSize: 15.sp,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
indicator: BoxDecoration(),
|
|
||||||
//TabBarIndicator(),
|
|
||||||
indicatorSize: TabBarIndicatorSize.label,
|
|
||||||
isScrollable: true,
|
|
||||||
tabAlignment: TabAlignment.start,
|
|
||||||
labelColor: SocialChatTheme.primaryLight,
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SCDebounceWidget(
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsetsDirectional.only(start: 10.w, end: 10.w),
|
|
||||||
child:
|
|
||||||
_isSearchingMessages
|
|
||||||
? Icon(Icons.close, size: 22.w, color: Colors.white)
|
|
||||||
: Image.asset(
|
|
||||||
"sc_images/index/sc_icon_serach.png",
|
|
||||||
width: 20.w,
|
|
||||||
height: 20.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (_isSearchingMessages) {
|
|
||||||
_stopMessageSearch();
|
|
||||||
} else {
|
|
||||||
_startMessageSearch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: TabBarView(
|
|
||||||
children: [
|
children: [
|
||||||
Consumer<RtmProvider>(
|
_buildTopBar(),
|
||||||
builder: (_, provider, __) {
|
Expanded(
|
||||||
return Container(
|
child: Consumer<RtmProvider>(
|
||||||
margin: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
builder: (_, provider, __) {
|
||||||
decoration: BoxDecoration(
|
return Column(
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
"sc_images/index/sc_index_msg_content_bg.png",
|
|
||||||
),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
if (!_isSearchingMessages) _headMenu(provider),
|
if (!_isSearchingMessages) _headMenu(provider),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Padding(
|
||||||
margin: EdgeInsets.only(
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
top: 0.w,
|
|
||||||
left: 8.w,
|
|
||||||
right: 8.w,
|
|
||||||
),
|
|
||||||
child: MessageConversationListPage(
|
child: MessageConversationListPage(
|
||||||
false,
|
false,
|
||||||
searchQuery: _messageSearchText,
|
searchQuery: _messageSearchText,
|
||||||
@ -196,9 +124,9 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -208,6 +136,51 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTopBar() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44.w,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isSearchingMessages
|
||||||
|
? _messageSearchField()
|
||||||
|
: SCHomeShellGradientText(
|
||||||
|
SCAppLocalizations.of(context)!.message,
|
||||||
|
style: scHomeShellTitleStyle(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SCDebounceWidget(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 38.w,
|
||||||
|
height: 38.w,
|
||||||
|
child: Center(
|
||||||
|
child:
|
||||||
|
_isSearchingMessages
|
||||||
|
? Icon(Icons.close, size: 22.w, color: Colors.white)
|
||||||
|
: Image.asset(
|
||||||
|
"sc_images/index/sc_icon_serach.png",
|
||||||
|
width: 24.w,
|
||||||
|
height: 24.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (_isSearchingMessages) {
|
||||||
|
_stopMessageSearch();
|
||||||
|
} else {
|
||||||
|
_startMessageSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _messageSearchField() {
|
Widget _messageSearchField() {
|
||||||
return Container(
|
return Container(
|
||||||
height: 36.w,
|
height: 36.w,
|
||||||
@ -232,7 +205,7 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
focusNode: _messageSearchFocusNode,
|
focusNode: _messageSearchFocusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
cursorColor: SocialChatTheme.primaryLight,
|
cursorColor: SocialChatTheme.primaryLight,
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14.sp),
|
style: TextStyle(color: Colors.black, fontSize: 14.sp),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
"${SCAppLocalizations.of(context)!.search} ID/${SCAppLocalizations.of(context)!.userName}",
|
"${SCAppLocalizations.of(context)!.search} ID/${SCAppLocalizations.of(context)!.userName}",
|
||||||
@ -275,72 +248,42 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
|
|
||||||
Widget _headMenu(RtmProvider provider) {
|
Widget _headMenu(RtmProvider provider) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsetsDirectional.symmetric(
|
margin: EdgeInsets.fromLTRB(10.w, 25.w, 10.w, 10.w),
|
||||||
horizontal: 20.w,
|
height: 114.w,
|
||||||
).copyWith(top: 10.w),
|
decoration: BoxDecoration(
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
spacing: 15.w,
|
border: Border.all(
|
||||||
|
color: scHomeShellAccentColor.withValues(alpha: 0.2),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
const Color(0xFF111A1B).withValues(alpha: 0.86),
|
||||||
|
const Color(0xFF2C4D42).withValues(alpha: 0.68),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SCDebounceWidget(
|
_buildMessageShortcut(
|
||||||
child: Stack(
|
label: SCAppLocalizations.of(context)!.notifcation,
|
||||||
children: [
|
icon: _strategy.getMessagePageNotificationMessageIcon(),
|
||||||
Row(
|
unreadCount: provider.notifcationUnReadCount,
|
||||||
children: [
|
onTap: () {
|
||||||
SizedBox(height: 3.w),
|
provider.updateNotificationCount(0);
|
||||||
Image.asset(
|
SCNavigatorUtils.push(
|
||||||
_strategy.getMessagePageActivityMessageIcon(),
|
context,
|
||||||
width: 45.w,
|
SCChatRouter.notifcation,
|
||||||
height: 45.w,
|
replace: false,
|
||||||
),
|
);
|
||||||
SizedBox(width: 5.w),
|
},
|
||||||
Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
_buildMessageShortcut(
|
||||||
children: [
|
label: SCAppLocalizations.of(context)!.activity,
|
||||||
text(
|
icon: _strategy.getMessagePageActivityMessageIcon(),
|
||||||
SCAppLocalizations.of(context)!.activity,
|
unreadCount: provider.activityUnReadCount,
|
||||||
textColor: Colors.white,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
SizedBox(height: 3.w),
|
|
||||||
text(
|
|
||||||
"[${SCAppLocalizations.of(context)!.newMessage}]",
|
|
||||||
fontSize: 12.sp,
|
|
||||||
textColor: Colors.white54,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
PositionedDirectional(
|
|
||||||
bottom: 0,
|
|
||||||
end: 10.w,
|
|
||||||
child: Visibility(
|
|
||||||
visible: provider.activityUnReadCount > 0,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
height: 16.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
//gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]),
|
|
||||||
color: Colors.red,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
//borderRadius: BorderRadius.all(Radius.circular(7.w)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"${provider.activityUnReadCount > 99 ? '99+' : provider.activityUnReadCount}",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10.sp,
|
|
||||||
color: Color(0xffffffff),
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
provider.updateActivityCount(0);
|
provider.updateActivityCount(0);
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
@ -350,45 +293,10 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SCDebounceWidget(
|
_buildMessageShortcut(
|
||||||
child: Row(
|
label: SCAppLocalizations.of(context)!.system,
|
||||||
children: [
|
icon: _strategy.getMessagePageSystemMessageIcon(),
|
||||||
SizedBox(height: 3.w),
|
unreadCount: provider.systemUnReadCount,
|
||||||
Image.asset(
|
|
||||||
_strategy.getMessagePageSystemMessageIcon(),
|
|
||||||
width: 45.w,
|
|
||||||
height: 45.w,
|
|
||||||
),
|
|
||||||
SizedBox(width: 6.w),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
text(
|
|
||||||
SCAppLocalizations.of(context)!.system,
|
|
||||||
textColor: Colors.white,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
SizedBox(height: 3.w),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: text(
|
|
||||||
"[${SCAppLocalizations.of(context)!.newMessage}]",
|
|
||||||
fontSize: 12.sp,
|
|
||||||
textColor: Colors.white54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
_unreadBadge(provider.systemUnReadCount),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var conversation = provider.getPreferredSystemConversation();
|
var conversation = provider.getPreferredSystemConversation();
|
||||||
provider.updateSystemCount(0);
|
provider.updateSystemCount(0);
|
||||||
@ -401,55 +309,49 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SCDebounceWidget(
|
],
|
||||||
child: Row(
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessageShortcut({
|
||||||
|
required String label,
|
||||||
|
required String icon,
|
||||||
|
required int unreadCount,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Expanded(
|
||||||
|
child: SCDebounceWidget(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 3.w),
|
Image.asset(icon, width: 52.w, height: 52.w),
|
||||||
Image.asset(
|
if (unreadCount > 0)
|
||||||
_strategy.getMessagePageNotificationMessageIcon(),
|
PositionedDirectional(
|
||||||
width: 45.w,
|
top: -2.w,
|
||||||
height: 45.w,
|
end: -6.w,
|
||||||
),
|
child: _unreadBadge(unreadCount),
|
||||||
SizedBox(width: 6.w),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
text(
|
|
||||||
SCAppLocalizations.of(context)!.notifcation,
|
|
||||||
textColor: Colors.white,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
SizedBox(height: 3.w),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: text(
|
|
||||||
"[${SCAppLocalizations.of(context)!.newMessage}]",
|
|
||||||
fontSize: 12.sp,
|
|
||||||
textColor: Colors.white54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
_unreadBadge(provider.notifcationUnReadCount),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
SizedBox(height: 10.w),
|
||||||
provider.updateNotificationCount(0);
|
Text(
|
||||||
SCNavigatorUtils.push(
|
label,
|
||||||
context,
|
maxLines: 1,
|
||||||
SCChatRouter.notifcation,
|
overflow: TextOverflow.ellipsis,
|
||||||
replace: false,
|
style: TextStyle(
|
||||||
);
|
color: Colors.white,
|
||||||
},
|
fontSize: 14.sp,
|
||||||
),
|
fontWeight: FontWeight.w700,
|
||||||
],
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -460,7 +362,8 @@ class _MessagePageState extends State<SCMessagePage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
height: 16.w,
|
height: 18.w,
|
||||||
|
constraints: BoxConstraints(minWidth: 18.w),
|
||||||
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
|
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${unreadCount > 99 ? '99+' : unreadCount}",
|
"${unreadCount > 99 ? '99+' : unreadCount}",
|
||||||
|
|||||||
@ -53,6 +53,7 @@ import 'package:yumi/app/config/business_logic_strategy.dart';
|
|||||||
import 'package:yumi/app/constants/sc_screen.dart';
|
import 'package:yumi/app/constants/sc_screen.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_chat_bubble_message_data.dart';
|
||||||
import 'package:yumi/shared/tools/sc_keybord_util.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_message_notifier.dart';
|
||||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||||
@ -2623,6 +2624,17 @@ class _MessageItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextBubble(String content) {
|
Widget _buildTextBubble(String content) {
|
||||||
|
final snapshot = scDecodeChatBubbleMessageSnapshot(message.cloudCustomData);
|
||||||
|
if (snapshot.hasSnapshot) {
|
||||||
|
if (snapshot.imageUrl.isNotEmpty) {
|
||||||
|
return _buildVipTextBubble(
|
||||||
|
content: content,
|
||||||
|
imageUrl: snapshot.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _buildDefaultTextBubble(content);
|
||||||
|
}
|
||||||
|
|
||||||
final chatBubble =
|
final chatBubble =
|
||||||
(message.isSelf ?? false)
|
(message.isSelf ?? false)
|
||||||
? _currentUserChatBubble()
|
? _currentUserChatBubble()
|
||||||
@ -2697,8 +2709,8 @@ class _MessageItem extends StatelessWidget {
|
|||||||
260.w,
|
260.w,
|
||||||
);
|
);
|
||||||
final multilineMaxTextWidth = math.max(
|
final multilineMaxTextWidth = math.max(
|
||||||
82.w,
|
60.w,
|
||||||
multilineMaxBubbleWidth - 76.w,
|
multilineMaxBubbleWidth - 52.w,
|
||||||
);
|
);
|
||||||
final multilineTextPainter = TextPainter(
|
final multilineTextPainter = TextPainter(
|
||||||
text: TextSpan(text: content, style: textStyle),
|
text: TextSpan(text: content, style: textStyle),
|
||||||
@ -2728,34 +2740,24 @@ class _MessageItem extends StatelessWidget {
|
|||||||
required Size textSize,
|
required Size textSize,
|
||||||
required double maxBubbleWidth,
|
required double maxBubbleWidth,
|
||||||
}) {
|
}) {
|
||||||
final leftPadding = 34.w;
|
final leftPadding = 24.w;
|
||||||
final rightPadding = 42.w;
|
final rightPadding = 28.w;
|
||||||
final topPadding = 16.w;
|
final topPadding = 10.w;
|
||||||
final bottomPadding = 18.w;
|
final bottomPadding = 12.w;
|
||||||
final bubbleWidth = math
|
final bubbleWidth = math
|
||||||
.max(126.w, textSize.width + leftPadding + rightPadding)
|
.max(92.w, textSize.width + leftPadding + rightPadding)
|
||||||
.clamp(126.w, maxBubbleWidth);
|
.clamp(92.w, maxBubbleWidth);
|
||||||
final textMaxWidth = math.max(
|
final textMaxWidth = math.max(
|
||||||
64.w,
|
40.w,
|
||||||
bubbleWidth - leftPadding - rightPadding,
|
bubbleWidth - leftPadding - rightPadding,
|
||||||
);
|
);
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
text: TextSpan(text: content, style: textStyle),
|
text: TextSpan(text: content, style: textStyle),
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
)..layout(maxWidth: textMaxWidth);
|
)..layout(maxWidth: textMaxWidth);
|
||||||
final lineMetrics = textPainter.computeLineMetrics();
|
final textBoxHeight = textPainter.size.height + 2.w;
|
||||||
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(
|
final bubbleHeight = math.max(
|
||||||
76.w,
|
52.w,
|
||||||
textBoxHeight + topPadding + bottomPadding,
|
textBoxHeight + topPadding + bottomPadding,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2797,13 +2799,13 @@ class _MessageItem extends StatelessWidget {
|
|||||||
required String imageUrl,
|
required String imageUrl,
|
||||||
required TextStyle textStyle,
|
required TextStyle textStyle,
|
||||||
}) {
|
}) {
|
||||||
final leftPadding = 22.w;
|
final leftPadding = 16.w;
|
||||||
final rightPadding = 24.w;
|
final rightPadding = 18.w;
|
||||||
final topPadding = 12.w;
|
final topPadding = 8.w;
|
||||||
final bottomPadding = 16.w;
|
final bottomPadding = 10.w;
|
||||||
final minSkinWidth = 50.w;
|
final minSkinWidth = 56.w;
|
||||||
final minSkinHeight = 58.w;
|
final minSkinHeight = 52.w;
|
||||||
final textWidthSafety = 10.w;
|
final textWidthSafety = 4.w;
|
||||||
final maxTextWidth = width(220);
|
final maxTextWidth = width(220);
|
||||||
final maxBubbleWidth = maxTextWidth + leftPadding + rightPadding;
|
final maxBubbleWidth = maxTextWidth + leftPadding + rightPadding;
|
||||||
final textSize = _measureTextSize(
|
final textSize = _measureTextSize(
|
||||||
@ -2964,32 +2966,7 @@ class _MessageItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
||||||
final candidates = <String?>[
|
return scResolveChatBubbleImageUrl(resource);
|
||||||
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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _firstNonBlank(Iterable<String?> values) {
|
|
||||||
for (final value in values) {
|
|
||||||
final text = value?.trim();
|
|
||||||
if (text != null && text.isNotEmpty) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMsgItemMenu(BuildContext ct, String content) {
|
void _showMsgItemMenu(BuildContext ct, String content) {
|
||||||
|
|||||||
@ -1650,9 +1650,9 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
if (!request.isBackpackGiftRequest &&
|
if (!request.isBackpackGiftRequest &&
|
||||||
_isInsufficientMoneyError(e, errorMessage)) {
|
_isInsufficientMoneyError(e, errorMessage)) {
|
||||||
RoomGiftComboSendController().hide();
|
RoomGiftComboSendController().hide();
|
||||||
RoomRechargeBottomSheet.show(
|
// RoomRechargeBottomSheet.show(
|
||||||
navigatorKey.currentState?.context ?? context,
|
// navigatorKey.currentState?.context ?? context,
|
||||||
);
|
// );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SCTts.show(errorMessage);
|
SCTts.show(errorMessage);
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import 'package:yumi/modules/index/main_route.dart';
|
|||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
|
||||||
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
|
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
|
|
||||||
///Home页面
|
///Home页面
|
||||||
class SCIndexHomePage extends StatefulWidget {
|
class SCIndexHomePage extends StatefulWidget {
|
||||||
@ -69,6 +69,25 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildStyledTab(Widget tab, int index) {
|
||||||
|
final controller = _tabController!;
|
||||||
|
if (tab is! Tab || tab.text == null) return tab;
|
||||||
|
return Tab(
|
||||||
|
height: tab.height,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller.animation!,
|
||||||
|
builder: (context, child) {
|
||||||
|
if (controller.index != index) return child!;
|
||||||
|
return SCHomeShellGradientText(
|
||||||
|
tab.text!,
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(tab.text!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) {
|
if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) {
|
||||||
@ -87,6 +106,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
|||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
const Positioned.fill(child: SCHomeShellBackground()),
|
||||||
Scaffold(
|
Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@ -94,56 +114,71 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
|||||||
children: [
|
children: [
|
||||||
// 自定义顶部栏(包含状态栏和 TabBar)
|
// 自定义顶部栏(包含状态栏和 TabBar)
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Row(
|
bottom: false,
|
||||||
children: [
|
child: SizedBox(
|
||||||
// TabBar 直接放在这里
|
height: 44.w,
|
||||||
Expanded(
|
child: Row(
|
||||||
child: TabBar(
|
children: [
|
||||||
tabAlignment: TabAlignment.start,
|
// TabBar 直接放在这里
|
||||||
isScrollable: true,
|
Expanded(
|
||||||
splashFactory: NoSplash.splashFactory,
|
child: TabBar(
|
||||||
overlayColor: WidgetStateProperty.all(
|
tabAlignment: TabAlignment.start,
|
||||||
Colors.transparent,
|
isScrollable: true,
|
||||||
),
|
labelPadding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
indicator: const BoxDecoration(),
|
splashFactory: NoSplash.splashFactory,
|
||||||
labelStyle: TextStyle(
|
overlayColor: WidgetStateProperty.all(
|
||||||
fontWeight: FontWeight.bold,
|
Colors.transparent,
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 19.sp,
|
|
||||||
color: SocialChatTheme.primaryLight,
|
|
||||||
),
|
|
||||||
unselectedLabelStyle: TextStyle(
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
indicatorColor: Colors.transparent,
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
controller: _tabController,
|
|
||||||
tabs: _tabs,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Image.asset(
|
|
||||||
"sc_images/index/sc_icon_serach.png",
|
|
||||||
width: 25.w,
|
|
||||||
height: 25.w,
|
|
||||||
),
|
|
||||||
onPressed:
|
|
||||||
() => SCNavigatorUtils.push(
|
|
||||||
context,
|
|
||||||
SCMainRoute.mainSearch,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
indicator: const BoxDecoration(),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 28.sp,
|
||||||
|
color: scHomeShellGoldColor,
|
||||||
|
shadows: const [
|
||||||
|
Shadow(
|
||||||
|
color: Color(0xB3000000),
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
blurRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
indicatorColor: Colors.transparent,
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: List<Widget>.generate(
|
||||||
|
_tabs.length,
|
||||||
|
(index) => _buildStyledTab(_tabs[index], index),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Container(
|
||||||
],
|
padding: EdgeInsetsDirectional.only(end: 10.w),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Image.asset(
|
||||||
|
"sc_images/index/sc_icon_serach.png",
|
||||||
|
width: 25.w,
|
||||||
|
height: 25.w,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
() => SCNavigatorUtils.push(
|
||||||
|
context,
|
||||||
|
SCMainRoute.mainSearch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// TabBarView 直接作为剩余空间
|
// TabBarView 直接作为剩余空间
|
||||||
|
|||||||
@ -3,19 +3,13 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_live_audio_indicator.dart';
|
|
||||||
import 'package:yumi/app/constants/sc_screen.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
import '../mine/sc_home_mine_room_card.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
|
||||||
import '../mine/sc_home_mine_skeleton.dart';
|
import '../mine/sc_home_mine_skeleton.dart';
|
||||||
|
|
||||||
///关注房间
|
///加入的房间
|
||||||
class SCRoomFollowPage extends SCPageList {
|
class SCRoomFollowPage extends SCPageList {
|
||||||
const SCRoomFollowPage({super.key});
|
const SCRoomFollowPage({super.key});
|
||||||
|
|
||||||
@ -28,7 +22,6 @@ class _RoomFollowPageState
|
|||||||
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||||
|
|
||||||
String? lastId;
|
|
||||||
Timer? _roomListRefreshTimer;
|
Timer? _roomListRefreshTimer;
|
||||||
bool _isSilentRefreshingRooms = false;
|
bool _isSilentRefreshingRooms = false;
|
||||||
bool _wasTickerModeEnabled = false;
|
bool _wasTickerModeEnabled = false;
|
||||||
@ -41,10 +34,17 @@ class _RoomFollowPageState
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
enablePullUp = true;
|
enablePullUp = false;
|
||||||
backgroundColor = Colors.transparent;
|
backgroundColor = Colors.transparent;
|
||||||
isGridView = true;
|
isGridView = true;
|
||||||
gridViewCount = 2;
|
gridViewCount = 2;
|
||||||
|
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
childAspectRatio: 172 / 200,
|
||||||
|
mainAxisSpacing: 8.w,
|
||||||
|
crossAxisSpacing: 10.w,
|
||||||
|
);
|
||||||
|
padding = EdgeInsets.only(top: 8.w, bottom: 10.w);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
_startRoomListAutoRefresh();
|
_startRoomListAutoRefresh();
|
||||||
}
|
}
|
||||||
@ -97,202 +97,38 @@ class _RoomFollowPageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(FollowRoomRes roomRes) {
|
Widget buildItem(FollowRoomRes roomRes) {
|
||||||
return GestureDetector(
|
return SCHomeMineRoomCard(room: roomRes);
|
||||||
child: Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
|
||||||
color: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(3.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage("sc_images/index/sc_icon_room_bord.png"),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: netImage(
|
|
||||||
url: resolveRoomCoverUrl(
|
|
||||||
roomRes.roomProfile?.id,
|
|
||||||
roomRes.roomProfile?.roomCover,
|
|
||||||
),
|
|
||||||
defaultImg: kRoomCoverDefaultImg,
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
|
||||||
width: 200.w,
|
|
||||||
height: 200.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 6.w),
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
"sc_images/index/sc_icon_index_room_brd.png",
|
|
||||||
),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(width: 10.w),
|
|
||||||
Consumer<SCAppGeneralManager>(
|
|
||||||
builder: (_, provider, __) {
|
|
||||||
return netImage(
|
|
||||||
url:
|
|
||||||
"${provider.findCountryByName(roomRes.roomProfile?.countryName ?? "")?.nationalFlag}",
|
|
||||||
width: 20.w,
|
|
||||||
height: 13.w,
|
|
||||||
borderRadius: BorderRadius.circular(2.w),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 17.w,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, -0.6.w),
|
|
||||||
child: text(
|
|
||||||
roomRes.roomProfile?.roomName ?? "",
|
|
||||||
fontSize: 13.sp,
|
|
||||||
textColor: Color(0xffffffff),
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
lineHeight: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// (roomRes.roomProfile?.roomName?.length ?? 0) > 10
|
|
||||||
// ? Marquee(
|
|
||||||
// text: roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// scrollAxis: Axis.horizontal,
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// blankSpace: 20.0,
|
|
||||||
// velocity: 40.0,
|
|
||||||
// pauseAfterRound: Duration(seconds: 1),
|
|
||||||
// accelerationDuration: Duration(seconds: 1),
|
|
||||||
// accelerationCurve: Curves.easeOut,
|
|
||||||
// decelerationDuration: Duration(
|
|
||||||
// milliseconds: 500,
|
|
||||||
// ),
|
|
||||||
// decelerationCurve: Curves.easeOut,
|
|
||||||
// )
|
|
||||||
// : Text(
|
|
||||||
// roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// maxLines: 1,
|
|
||||||
// overflow: TextOverflow.ellipsis,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
|
||||||
: Image.asset(
|
|
||||||
"sc_images/index/sc_icon_room_suo.png",
|
|
||||||
width: 20.w,
|
|
||||||
height: 20.w,
|
|
||||||
),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? SizedBox(width: 3.w)
|
|
||||||
: Container(height: 10.w),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? text(
|
|
||||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
|
||||||
fontSize: 10.sp,
|
|
||||||
lineHeight: 1,
|
|
||||||
)
|
|
||||||
: Container(height: 10.w),
|
|
||||||
SizedBox(width: 10.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
getRoomCoverHeaddress(roomRes).isNotEmpty
|
|
||||||
? Transform.translate(
|
|
||||||
offset: Offset(0, -5.w),
|
|
||||||
child: Transform.scale(
|
|
||||||
scaleX: 1.1,
|
|
||||||
scaleY: 1.12,
|
|
||||||
child: Image.asset(getRoomCoverHeaddress(roomRes)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
|
|
||||||
(roomRes.roomProfile?.roomGameIcon?.isNotEmpty ?? false)
|
|
||||||
? PositionedDirectional(
|
|
||||||
top: 8.w,
|
|
||||||
end: 8.w,
|
|
||||||
child: netImage(
|
|
||||||
url: roomRes.roomProfile?.roomGameIcon ?? "",
|
|
||||||
width: 25.w,
|
|
||||||
height: 25.w,
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
|
|
||||||
context,
|
|
||||||
roomRes.roomProfile?.id ?? "",
|
|
||||||
previewData: RoomEntryPreviewData.fromFollowRoom(roomRes),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getRoomCoverHeaddress(FollowRoomRes roomRes) {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
empty() {
|
empty() {
|
||||||
List<Widget> list = [];
|
return Align(
|
||||||
list.add(SizedBox(height: height(30)));
|
alignment: Alignment.topCenter,
|
||||||
list.add(
|
child: Container(
|
||||||
Image.asset(
|
width: double.infinity,
|
||||||
'sc_images/general/sc_icon_loading.png',
|
height: 76.w,
|
||||||
width: 120.w,
|
alignment: Alignment.center,
|
||||||
height: 120.w,
|
decoration: BoxDecoration(
|
||||||
),
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
);
|
border: Border.all(color: const Color(0x3306FFC0)),
|
||||||
list.add(SizedBox(height: height(15)));
|
gradient: const LinearGradient(
|
||||||
list.add(
|
begin: Alignment.topLeft,
|
||||||
Text(
|
end: Alignment.bottomRight,
|
||||||
SCAppLocalizations.of(context)!.youHaventFollowed,
|
colors: [Color(0x99111A1B), Color(0x992C4D42)],
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: sp(14),
|
),
|
||||||
color: Color(0xff999999),
|
child: Text(
|
||||||
fontWeight: FontWeight.w400,
|
SCAppLocalizations.of(context)!.youHaventJoinedAnyRoomsYet,
|
||||||
decoration: TextDecoration.none,
|
style: TextStyle(
|
||||||
height: 1,
|
fontSize: 14.sp,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
height: 20 / 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: list,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///加载数据
|
///加载数据
|
||||||
@ -302,14 +138,8 @@ class _RoomFollowPageState
|
|||||||
required Function(List<FollowRoomRes>) onSuccess,
|
required Function(List<FollowRoomRes>) onSuccess,
|
||||||
Function? onErr,
|
Function? onErr,
|
||||||
}) async {
|
}) async {
|
||||||
if (page == 1) {
|
|
||||||
lastId = null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
var roomList = await SCAccountRepository().followRoomList(lastId: lastId);
|
final roomList = await SCAccountRepository().joined();
|
||||||
if (roomList.isNotEmpty) {
|
|
||||||
lastId = roomList.last.id;
|
|
||||||
}
|
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
_hasCompletedInitialLoad = true;
|
_hasCompletedInitialLoad = true;
|
||||||
}
|
}
|
||||||
@ -369,7 +199,6 @@ class _RoomFollowPageState
|
|||||||
items
|
items
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(latestRooms);
|
..addAll(latestRooms);
|
||||||
lastId = latestRooms.isNotEmpty ? latestRooms.last.id : null;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,7 +220,7 @@ class _RoomFollowPageState
|
|||||||
}
|
}
|
||||||
_isSilentRefreshingRooms = true;
|
_isSilentRefreshingRooms = true;
|
||||||
try {
|
try {
|
||||||
final latestRooms = await SCAccountRepository().followRoomList();
|
final latestRooms = await SCAccountRepository().joined();
|
||||||
if (!mounted || !TickerMode.valuesOf(context).enabled) {
|
if (!mounted || !TickerMode.valuesOf(context).enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,15 +4,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_live_audio_indicator.dart';
|
|
||||||
import 'package:yumi/app/constants/sc_screen.dart';
|
|
||||||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
import '../mine/sc_home_mine_room_card.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
|
||||||
import '../mine/sc_home_mine_skeleton.dart';
|
import '../mine/sc_home_mine_skeleton.dart';
|
||||||
|
|
||||||
///历史房间
|
///历史房间
|
||||||
@ -45,6 +39,13 @@ class _SCRoomHistoryPageState
|
|||||||
backgroundColor = Colors.transparent;
|
backgroundColor = Colors.transparent;
|
||||||
isGridView = true;
|
isGridView = true;
|
||||||
gridViewCount = 2;
|
gridViewCount = 2;
|
||||||
|
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
childAspectRatio: 172 / 200,
|
||||||
|
mainAxisSpacing: 8.w,
|
||||||
|
crossAxisSpacing: 10.w,
|
||||||
|
);
|
||||||
|
padding = EdgeInsets.only(top: 8.w, bottom: 10.w);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
_startRoomListAutoRefresh();
|
_startRoomListAutoRefresh();
|
||||||
}
|
}
|
||||||
@ -97,202 +98,38 @@ class _SCRoomHistoryPageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(FollowRoomRes roomRes) {
|
Widget buildItem(FollowRoomRes roomRes) {
|
||||||
return GestureDetector(
|
return SCHomeMineRoomCard(room: roomRes);
|
||||||
child: Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
|
||||||
color: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(3.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage("sc_images/index/sc_icon_room_bord.png"),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: netImage(
|
|
||||||
url: resolveRoomCoverUrl(
|
|
||||||
roomRes.roomProfile?.id,
|
|
||||||
roomRes.roomProfile?.roomCover,
|
|
||||||
),
|
|
||||||
defaultImg: kRoomCoverDefaultImg,
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
|
||||||
width: 200.w,
|
|
||||||
height: 200.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 6.w),
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
"sc_images/index/sc_icon_index_room_brd.png",
|
|
||||||
),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(width: 10.w),
|
|
||||||
Consumer<SCAppGeneralManager>(
|
|
||||||
builder: (_, provider, __) {
|
|
||||||
return netImage(
|
|
||||||
url:
|
|
||||||
"${provider.findCountryByName(roomRes.roomProfile?.countryName ?? "")?.nationalFlag}",
|
|
||||||
width: 20.w,
|
|
||||||
height: 13.w,
|
|
||||||
borderRadius: BorderRadius.circular(2.w),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 17.w,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, -0.6.w),
|
|
||||||
child: text(
|
|
||||||
roomRes.roomProfile?.roomName ?? "",
|
|
||||||
fontSize: 13.sp,
|
|
||||||
textColor: Color(0xffffffff),
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
lineHeight: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// (roomRes.roomProfile?.roomName?.length ?? 0) > 10
|
|
||||||
// ? Marquee(
|
|
||||||
// text: roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// scrollAxis: Axis.horizontal,
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// blankSpace: 20.0,
|
|
||||||
// velocity: 40.0,
|
|
||||||
// pauseAfterRound: Duration(seconds: 1),
|
|
||||||
// accelerationDuration: Duration(seconds: 1),
|
|
||||||
// accelerationCurve: Curves.easeOut,
|
|
||||||
// decelerationDuration: Duration(
|
|
||||||
// milliseconds: 500,
|
|
||||||
// ),
|
|
||||||
// decelerationCurve: Curves.easeOut,
|
|
||||||
// )
|
|
||||||
// : Text(
|
|
||||||
// roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// maxLines: 1,
|
|
||||||
// overflow: TextOverflow.ellipsis,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
|
||||||
: Image.asset(
|
|
||||||
"sc_images/index/sc_icon_room_suo.png",
|
|
||||||
width: 20.w,
|
|
||||||
height: 20.w,
|
|
||||||
),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? SizedBox(width: 3.w)
|
|
||||||
: Container(height: 10.w),
|
|
||||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
|
||||||
? text(
|
|
||||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
|
||||||
fontSize: 10.sp,
|
|
||||||
lineHeight: 1,
|
|
||||||
)
|
|
||||||
: Container(height: 10.w),
|
|
||||||
SizedBox(width: 10.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
getRoomCoverHeaddress(roomRes).isNotEmpty
|
|
||||||
? Transform.translate(
|
|
||||||
offset: Offset(0, -5.w),
|
|
||||||
child: Transform.scale(
|
|
||||||
scaleX: 1.1,
|
|
||||||
scaleY: 1.12,
|
|
||||||
child: Image.asset(getRoomCoverHeaddress(roomRes)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
(roomRes.roomProfile?.roomGameIcon?.isNotEmpty ?? false)
|
|
||||||
? PositionedDirectional(
|
|
||||||
top: 8.w,
|
|
||||||
end: 8.w,
|
|
||||||
child: netImage(
|
|
||||||
url: roomRes.roomProfile?.roomGameIcon ?? "",
|
|
||||||
width: 25.w,
|
|
||||||
height: 25.w,
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
|
|
||||||
context,
|
|
||||||
roomRes.roomProfile?.id ?? "",
|
|
||||||
previewData: RoomEntryPreviewData.fromFollowRoom(roomRes),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
empty() {
|
empty() {
|
||||||
List<Widget> list = [];
|
return Align(
|
||||||
list.add(SizedBox(height: height(30)));
|
alignment: Alignment.topCenter,
|
||||||
list.add(
|
child: Container(
|
||||||
Image.asset(
|
width: double.infinity,
|
||||||
'sc_images/general/sc_icon_loading.png',
|
height: 76.w,
|
||||||
width: 120.w,
|
alignment: Alignment.center,
|
||||||
height: 120.w,
|
decoration: BoxDecoration(
|
||||||
),
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
);
|
border: Border.all(color: const Color(0x3306FFC0)),
|
||||||
list.add(SizedBox(height: height(15)));
|
gradient: const LinearGradient(
|
||||||
list.add(
|
begin: Alignment.topLeft,
|
||||||
Text(
|
end: Alignment.bottomRight,
|
||||||
SCAppLocalizations.of(context)!.noData,
|
colors: [Color(0x99111A1B), Color(0x992C4D42)],
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: sp(14),
|
),
|
||||||
color: Color(0xff999999),
|
child: Text(
|
||||||
fontWeight: FontWeight.w400,
|
SCAppLocalizations.of(context)!.noRoomRecordsRecently,
|
||||||
decoration: TextDecoration.none,
|
style: TextStyle(
|
||||||
height: 1,
|
fontSize: 14.sp,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
height: 20 / 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: list,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getRoomCoverHeaddress(FollowRoomRes roomRes) {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///加载数据
|
///加载数据
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import '../../../../services/general/sc_app_general_manager.dart';
|
|||||||
import '../../../../services/audio/rtc_manager.dart';
|
import '../../../../services/audio/rtc_manager.dart';
|
||||||
import '../../../../services/room/rc_room_manager.dart';
|
import '../../../../services/room/rc_room_manager.dart';
|
||||||
import '../../../../ui_kit/components/sc_compontent.dart';
|
import '../../../../ui_kit/components/sc_compontent.dart';
|
||||||
import '../../../../ui_kit/theme/socialchat_theme.dart';
|
import '../../../../ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
import '../follow/sc_room_follow_page.dart';
|
import '../follow/sc_room_follow_page.dart';
|
||||||
import '../history/sc_room_history_page.dart';
|
import '../history/sc_room_history_page.dart';
|
||||||
import 'sc_home_mine_skeleton.dart';
|
import 'sc_home_mine_skeleton.dart';
|
||||||
@ -51,72 +51,79 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
_tabs.clear();
|
_tabs.clear();
|
||||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.recent));
|
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.recent));
|
||||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.followed));
|
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.joined));
|
||||||
return Column(
|
return Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
children: [
|
child: Column(
|
||||||
Consumer<SocialChatRoomManager>(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
builder: (_, provider, __) {
|
children: [
|
||||||
if (provider.isMyRoomLoading && provider.myRoom == null) {
|
_buildSectionLabel(SCAppLocalizations.of(context)!.myRoom),
|
||||||
return const SCHomeSkeletonShimmer(
|
Consumer<SocialChatRoomManager>(
|
||||||
builder: _buildMineRoomSkeletonContent,
|
builder: (_, provider, __) {
|
||||||
|
if (provider.isMyRoomLoading && provider.myRoom == null) {
|
||||||
|
return const SCHomeSkeletonShimmer(
|
||||||
|
builder: _buildMineRoomSkeletonContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 85.w,
|
||||||
|
child: _buildMyRoom(provider),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 12.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
provider.myRoom == null
|
|
||||||
? "sc_images/index/sc_icon_my_room_no_bg.png"
|
|
||||||
: "sc_images/index/sc_icon_my_room_has_bg.png",
|
|
||||||
),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
width: ScreenUtil().screenWidth,
|
|
||||||
height: 85.w,
|
|
||||||
child: _buildMyRoom(provider),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(height: 5.w),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
TabBar(
|
|
||||||
tabAlignment: TabAlignment.start,
|
|
||||||
labelPadding: EdgeInsets.symmetric(horizontal: 12.w),
|
|
||||||
labelColor: SocialChatTheme.primaryLight,
|
|
||||||
isScrollable: true,
|
|
||||||
splashFactory: NoSplash.splashFactory,
|
|
||||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
||||||
indicator: BoxDecoration(),
|
|
||||||
unselectedLabelColor: Colors.white,
|
|
||||||
labelStyle: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 19.sp,
|
|
||||||
),
|
|
||||||
unselectedLabelStyle: TextStyle(
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
),
|
|
||||||
indicatorColor: Colors.transparent,
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
controller: _tabController,
|
|
||||||
tabs: _tabs,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
children: _pages,
|
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
tabAlignment: TabAlignment.start,
|
||||||
|
labelPadding: EdgeInsetsDirectional.only(end: 18.w),
|
||||||
|
labelColor: scHomeShellAccentColor,
|
||||||
|
isScrollable: true,
|
||||||
|
splashFactory: NoSplash.splashFactory,
|
||||||
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
|
indicator: BoxDecoration(),
|
||||||
|
unselectedLabelColor: Colors.white,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
),
|
||||||
|
indicatorColor: Colors.transparent,
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: _tabs,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.w),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
children: _pages,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionLabel(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 8.w, bottom: 10.w),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: scHomeShellAccentColor,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
height: 1.4,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,61 +138,58 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
|||||||
return provider.myRoom != null
|
return provider.myRoom != null
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Row(
|
child: _buildMineRoomShell(
|
||||||
children: [
|
child: Row(
|
||||||
SizedBox(width: 10.w),
|
children: [
|
||||||
netImage(
|
netImage(
|
||||||
url: resolveRoomCoverUrl(
|
url: resolveRoomCoverUrl(
|
||||||
provider.myRoom?.id,
|
provider.myRoom?.id,
|
||||||
provider.myRoom?.roomCover,
|
provider.myRoom?.roomCover,
|
||||||
|
),
|
||||||
|
defaultImg: kRoomCoverDefaultImg,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
||||||
|
width: 56.w,
|
||||||
|
height: 56.w,
|
||||||
),
|
),
|
||||||
defaultImg: kRoomCoverDefaultImg,
|
SizedBox(width: 10.w),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
Expanded(
|
||||||
width: 70.w,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
SizedBox(width: 10.w),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Expanded(
|
children: [
|
||||||
child: Stack(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
netImage(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
url:
|
||||||
children: [
|
Provider.of<SCAppGeneralManager>(
|
||||||
SizedBox(height: 6.w),
|
context,
|
||||||
Row(
|
listen: false,
|
||||||
children: [
|
)
|
||||||
netImage(
|
.findCountryByName(
|
||||||
url:
|
AccountStorage()
|
||||||
Provider.of<SCAppGeneralManager>(
|
.getCurrentUser()
|
||||||
context,
|
?.userProfile
|
||||||
listen: false,
|
?.countryName ??
|
||||||
)
|
"",
|
||||||
.findCountryByName(
|
)
|
||||||
AccountStorage()
|
?.nationalFlag ??
|
||||||
.getCurrentUser()
|
"",
|
||||||
?.userProfile
|
borderRadius: BorderRadius.all(
|
||||||
?.countryName ??
|
Radius.circular(3.w),
|
||||||
"",
|
|
||||||
)
|
|
||||||
?.nationalFlag ??
|
|
||||||
"",
|
|
||||||
borderRadius: BorderRadius.all(
|
|
||||||
Radius.circular(3.w),
|
|
||||||
),
|
|
||||||
width: 19.w,
|
|
||||||
height: 14.w,
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 3.w),
|
width: 19.w,
|
||||||
Container(
|
height: 14.w,
|
||||||
constraints: BoxConstraints(
|
),
|
||||||
maxWidth: 200.w,
|
SizedBox(width: 3.w),
|
||||||
maxHeight: 24.w,
|
Expanded(
|
||||||
),
|
child: SizedBox(
|
||||||
|
height: 20.w,
|
||||||
child:
|
child:
|
||||||
(provider.myRoom?.roomName?.length ?? 0) > 18
|
(provider.myRoom?.roomName?.length ?? 0) > 18
|
||||||
? Marquee(
|
? Marquee(
|
||||||
text: provider.myRoom?.roomName ?? "",
|
text: provider.myRoom?.roomName ?? "",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16.sp,
|
fontSize: 14.sp,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
@ -210,32 +214,36 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16.sp,
|
fontSize: 14.sp,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
text(
|
),
|
||||||
provider.myRoom?.roomDesc ?? "",
|
text(
|
||||||
fontSize: 14.sp,
|
provider.myRoom?.roomDesc ?? "",
|
||||||
textColor: Colors.white,
|
fontSize: 14.sp,
|
||||||
),
|
textColor: Colors.white.withValues(alpha: 0.72),
|
||||||
text(
|
),
|
||||||
"ID:${provider.myRoom?.roomAccount}",
|
text(
|
||||||
fontSize: 12.sp,
|
"ID:${provider.myRoom?.roomAccount}",
|
||||||
textColor: Colors.white,
|
fontSize: 10.sp,
|
||||||
),
|
textColor: Colors.white.withValues(alpha: 0.64),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
Icon(
|
||||||
SizedBox(width: 12.w),
|
Icons.chevron_right,
|
||||||
],
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
|
size: 26.w,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final roomManager = Provider.of<SocialChatRoomManager>(
|
final roomManager = Provider.of<SocialChatRoomManager>(
|
||||||
@ -262,51 +270,78 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: GestureDetector(
|
: GestureDetector(
|
||||||
child: Row(
|
child: _buildMineRoomShell(
|
||||||
children: [
|
child: Row(
|
||||||
SizedBox(width: 10.w),
|
children: [
|
||||||
Image.asset(
|
Container(
|
||||||
"sc_images/index/sc_icon_index_creat_room_tag.png",
|
width: 56.w,
|
||||||
height: 70.w,
|
height: 56.w,
|
||||||
width: 70.w,
|
alignment: Alignment.center,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
SizedBox(width: 10.w),
|
color: scHomeShellAccentColor.withValues(alpha: 0.1),
|
||||||
Expanded(
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Icon(Icons.add, color: Colors.white, size: 28.w),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
text(
|
|
||||||
SCAppLocalizations.of(context)!.crateMyRoom,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
textColor: Colors.white,
|
|
||||||
),
|
|
||||||
text(
|
|
||||||
SCAppLocalizations.of(context)!.startYourBrandNewJourney,
|
|
||||||
fontSize: 12.sp,
|
|
||||||
textColor: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 10.w),
|
||||||
Image.asset(
|
Expanded(
|
||||||
"sc_images/index/sc_icon_my_room_tag2.png",
|
child: Column(
|
||||||
height: 25.w,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
width: 25.w,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
SizedBox(width: 10.w),
|
text(
|
||||||
Icon(
|
SCAppLocalizations.of(context)!.crateMyRoom,
|
||||||
Icons.chevron_right_outlined,
|
fontSize: 14.sp,
|
||||||
color: Colors.white,
|
fontWeight: FontWeight.w800,
|
||||||
size: 20.w,
|
textColor: Colors.white,
|
||||||
),
|
),
|
||||||
SizedBox(width: 15.w),
|
SizedBox(height: 4.w),
|
||||||
],
|
text(
|
||||||
|
SCAppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.startYourBrandNewJourney,
|
||||||
|
fontSize: 10.sp,
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Image.asset(
|
||||||
|
"sc_images/index/sc_icon_my_room_tag2.png",
|
||||||
|
height: 30.w,
|
||||||
|
width: 30.w,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Icon(Icons.chevron_right, color: Colors.white, size: 26.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
provider.createNewRoom(context);
|
provider.createNewRoom(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMineRoomShell({required Widget child}) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(10.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
|
border: Border.all(
|
||||||
|
color: scHomeShellAccentColor.withValues(alpha: 0.2),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
const Color(0xFF111A1B).withValues(alpha: 0.86),
|
||||||
|
const Color(0xFF2C4D42).withValues(alpha: 0.72),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
lib/modules/home/popular/mine/sc_home_mine_room_card.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/room_live_audio_indicator.dart';
|
||||||
|
|
||||||
|
const Color _kHomeMineRoomAccent = Color(0xFF18F2B1);
|
||||||
|
|
||||||
|
class SCHomeMineRoomCard extends StatelessWidget {
|
||||||
|
const SCHomeMineRoomCard({super.key, required this.room});
|
||||||
|
|
||||||
|
final FollowRoomRes room;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final profile = room.roomProfile;
|
||||||
|
return SCDebounceWidget(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF222222),
|
||||||
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
|
border: Border.all(
|
||||||
|
color: _kHomeMineRoomAccent.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: netImage(
|
||||||
|
url: resolveRoomCoverUrl(
|
||||||
|
profile?.id,
|
||||||
|
profile?.roomCover,
|
||||||
|
),
|
||||||
|
defaultImg: kRoomCoverDefaultImg,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
|
border: Border.all(
|
||||||
|
color: _kHomeMineRoomAccent.withValues(alpha: 0.2),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 8.w,
|
||||||
|
bottom: 9.w,
|
||||||
|
child:
|
||||||
|
profile?.extValues?.existsPassword != true
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
SCRoomLiveAudioIndicator(
|
||||||
|
width: 14.w,
|
||||||
|
height: 14.w,
|
||||||
|
),
|
||||||
|
SizedBox(width: 3.w),
|
||||||
|
text(
|
||||||
|
profile?.displayMemberCount ?? "0",
|
||||||
|
fontSize: 10.sp,
|
||||||
|
lineHeight: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Image.asset(
|
||||||
|
"sc_images/index/sc_icon_room_suo.png",
|
||||||
|
width: 20.w,
|
||||||
|
height: 20.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.w),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Consumer<SCAppGeneralManager>(
|
||||||
|
builder: (_, provider, __) {
|
||||||
|
final flagUrl =
|
||||||
|
_trimToNull(profile?.nationalFlag) ??
|
||||||
|
_trimToNull(
|
||||||
|
provider
|
||||||
|
.findCountryByName(profile?.countryName ?? "")
|
||||||
|
?.nationalFlag,
|
||||||
|
);
|
||||||
|
if (flagUrl == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: 4.w),
|
||||||
|
child: netImage(
|
||||||
|
url: flagUrl,
|
||||||
|
width: 22.w,
|
||||||
|
height: 16.w,
|
||||||
|
borderRadius: BorderRadius.circular(3.w),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(profile?.roomName ?? "").toUpperCase(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 16 / 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
|
||||||
|
context,
|
||||||
|
profile?.id ?? "",
|
||||||
|
previewData: RoomEntryPreviewData.fromFollowRoom(room),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _trimToNull(String? value) {
|
||||||
|
final normalized = value?.trim();
|
||||||
|
return normalized == null || normalized.isEmpty ? null : normalized;
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import 'package:carousel_slider/carousel_slider.dart';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
@ -19,7 +20,6 @@ import '../../../../services/home/home_room_preload_manager.dart';
|
|||||||
import '../../../../ui_kit/components/sc_compontent.dart';
|
import '../../../../ui_kit/components/sc_compontent.dart';
|
||||||
import '../../../../ui_kit/components/text/sc_text.dart';
|
import '../../../../ui_kit/components/text/sc_text.dart';
|
||||||
import '../../../../ui_kit/widgets/room/room_live_audio_indicator.dart';
|
import '../../../../ui_kit/widgets/room/room_live_audio_indicator.dart';
|
||||||
import '../../../../ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
|
||||||
import '../../../index/main_route.dart';
|
import '../../../index/main_route.dart';
|
||||||
|
|
||||||
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
|
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
|
||||||
@ -28,6 +28,7 @@ const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41);
|
|||||||
const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53);
|
const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53);
|
||||||
const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177);
|
const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177);
|
||||||
const Color _kPartySkeletonBorder = Color(0x66D8B57B);
|
const Color _kPartySkeletonBorder = Color(0x66D8B57B);
|
||||||
|
const Color _kPartyAccent = Color(0xFF18F2B1);
|
||||||
|
|
||||||
class _RoomCountryFilterOption {
|
class _RoomCountryFilterOption {
|
||||||
const _RoomCountryFilterOption({
|
const _RoomCountryFilterOption({
|
||||||
@ -56,11 +57,20 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||||
static const Duration _roomCounterHydrationMinGap = Duration(seconds: 20);
|
static const Duration _roomCounterHydrationMinGap = Duration(seconds: 20);
|
||||||
static const int _roomCounterHydrationLimit = 6;
|
static const int _roomCounterHydrationLimit = 6;
|
||||||
static const List<String> _topRankBorderAssets = <String>[
|
static const String _wealthRankBackgroundAsset =
|
||||||
"sc_images/index/sc_icon_home_room_rank_border_1.svga",
|
'sc_images/index/sc_party_rank_wealth_bg.png';
|
||||||
"sc_images/index/sc_icon_home_room_rank_border_2.svga",
|
static const String _charmRankBackgroundAsset =
|
||||||
"sc_images/index/sc_icon_home_room_rank_border_3.svga",
|
'sc_images/index/sc_party_rank_charm_bg.png';
|
||||||
];
|
static const String _roomRankBackgroundAsset =
|
||||||
|
'sc_images/index/sc_party_rank_room_bg.png';
|
||||||
|
static const String _rankFrameGoldAsset =
|
||||||
|
'sc_images/index/sc_party_rank_frame_gold.png';
|
||||||
|
static const String _rankFrameSilverAsset =
|
||||||
|
'sc_images/index/sc_party_rank_frame_silver.png';
|
||||||
|
static const String _rankFrameBronzeAsset =
|
||||||
|
'sc_images/index/sc_party_rank_frame_bronze.png';
|
||||||
|
static const String _rankRibbonsAsset =
|
||||||
|
'sc_images/index/sc_party_rank_ribbons.svg';
|
||||||
|
|
||||||
List<FollowRoomRes> historyRooms = [];
|
List<FollowRoomRes> historyRooms = [];
|
||||||
String? lastId;
|
String? lastId;
|
||||||
@ -140,6 +150,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
},
|
},
|
||||||
onLoading: () {},
|
onLoading: () {},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(top: 10.w, bottom: 10.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -149,7 +160,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: 12.w),
|
padding: EdgeInsets.only(bottom: 8.w),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children: [
|
children: [
|
||||||
@ -425,7 +436,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
children: [
|
children: [
|
||||||
CarouselSlider(
|
CarouselSlider(
|
||||||
options: CarouselOptions(
|
options: CarouselOptions(
|
||||||
height: 95.w,
|
height: 100.w,
|
||||||
autoPlay: hasMultipleBanners,
|
autoPlay: hasMultipleBanners,
|
||||||
// 启用自动播放
|
// 启用自动播放
|
||||||
enlargeCenterPage: false,
|
enlargeCenterPage: false,
|
||||||
@ -448,12 +459,20 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
items:
|
items:
|
||||||
banners.map((item) {
|
banners.map((item) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: netImage(
|
child: Container(
|
||||||
url: item.cover ?? "",
|
width: ScreenUtil().screenWidth - 20.w,
|
||||||
height: 95.w,
|
decoration: BoxDecoration(
|
||||||
width: ScreenUtil().screenWidth * 0.9,
|
color: Colors.white,
|
||||||
fit: BoxFit.fill,
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: netImage(
|
||||||
|
url: item.cover ?? "",
|
||||||
|
height: 100.w,
|
||||||
|
width: ScreenUtil().screenWidth - 20.w,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCBannerUtils.openBanner(item, context);
|
SCBannerUtils.openBanner(item, context);
|
||||||
@ -466,13 +485,15 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
children:
|
children:
|
||||||
banners.asMap().entries.map((entry) {
|
banners.asMap().entries.map((entry) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 6.0,
|
width: _currentIndex == entry.key ? 10.w : 4.w,
|
||||||
height: 6.0,
|
height: 3.w,
|
||||||
margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
margin: EdgeInsets.symmetric(vertical: 6.w, horizontal: 2.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
borderRadius: BorderRadius.circular(999.w),
|
||||||
color:
|
color:
|
||||||
_currentIndex == entry.key ? Colors.blue : Colors.white,
|
_currentIndex == entry.key
|
||||||
|
? _kPartyAccent
|
||||||
|
: _kPartyAccent.withValues(alpha: 0.45),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@ -579,113 +600,66 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
charmOneAvatar.isEmpty) {
|
charmOneAvatar.isEmpty) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return SizedBox(
|
return Padding(
|
||||||
height: 130.w,
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
_buildLargeLeaderboardCard(
|
||||||
child: GestureDetector(
|
title: SCAppLocalizations.of(context)!.leaderboardWealth,
|
||||||
child: Stack(
|
borderColor: const Color(0xFF05C58A),
|
||||||
alignment: AlignmentDirectional.center,
|
gradientColors: const [Color(0xFF235045), Color(0xFF152121)],
|
||||||
children: [
|
imageAsset: _wealthRankBackgroundAsset,
|
||||||
PositionedDirectional(
|
avatars: [wealthTwoAvatar, wealthOneAvatar, wealthThreeAvatar],
|
||||||
start: 26.w,
|
onTap: () {
|
||||||
top: 65.w,
|
SCNavigatorUtils.push(
|
||||||
child: head(url: wealthTwoAvatar, width: 30.w),
|
context,
|
||||||
),
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false",
|
||||||
Positioned(
|
replace: false,
|
||||||
top: 35.w,
|
);
|
||||||
child: head(url: wealthOneAvatar, width: 36.w),
|
},
|
||||||
),
|
|
||||||
PositionedDirectional(
|
|
||||||
end: 24.w,
|
|
||||||
top: 65.w,
|
|
||||||
child: head(url: wealthThreeAvatar, width: 30.w),
|
|
||||||
),
|
|
||||||
Image.asset(
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getPopularLeaderboardBackgroundImage('wealth'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
SCNavigatorUtils.push(
|
|
||||||
context,
|
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false",
|
|
||||||
replace: false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 11.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: Column(
|
||||||
child: Stack(
|
children: [
|
||||||
alignment: AlignmentDirectional.center,
|
_buildSmallLeaderboardCard(
|
||||||
children: [
|
title: SCAppLocalizations.of(context)!.leaderboardCharm,
|
||||||
PositionedDirectional(
|
borderColor: const Color(0xFF6800E1),
|
||||||
start: 24.w,
|
gradientColors: const [Color(0x99632CBA), Color(0x998747CB)],
|
||||||
top: 68.w,
|
imageAsset: _charmRankBackgroundAsset,
|
||||||
child: head(url: roomGiftTwoAvatar, width: 30.w),
|
imageTop: -3,
|
||||||
),
|
imageEnd: 13,
|
||||||
Positioned(
|
avatars: [charmTwoAvatar, charmOneAvatar, charmThreeAvatar],
|
||||||
top: 35.w,
|
onTap: () {
|
||||||
child: head(url: roomGiftOneAvatar, width: 38.w),
|
SCNavigatorUtils.push(
|
||||||
),
|
context,
|
||||||
PositionedDirectional(
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false",
|
||||||
end: 25.w,
|
replace: false,
|
||||||
top: 69.w,
|
);
|
||||||
child: head(url: roomGiftThreeAvatar, width: 30.w),
|
},
|
||||||
),
|
),
|
||||||
Image.asset(
|
SizedBox(height: 10.w),
|
||||||
SCGlobalConfig.businessLogicStrategy
|
_buildSmallLeaderboardCard(
|
||||||
.getPopularLeaderboardBackgroundImage('room'),
|
title: SCAppLocalizations.of(context)!.leaderboardRoom,
|
||||||
fit: BoxFit.fill,
|
borderColor: const Color(0xFF0A88F5),
|
||||||
),
|
gradientColors: const [Color(0xCC2169A3), Color(0xCC2F7F9D)],
|
||||||
],
|
imageAsset: _roomRankBackgroundAsset,
|
||||||
),
|
imageTop: 2,
|
||||||
onTap: () {
|
imageEnd: 8,
|
||||||
SCNavigatorUtils.push(
|
avatars: [
|
||||||
context,
|
roomGiftTwoAvatar,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false",
|
roomGiftOneAvatar,
|
||||||
replace: false,
|
roomGiftThreeAvatar,
|
||||||
);
|
],
|
||||||
},
|
onTap: () {
|
||||||
),
|
SCNavigatorUtils.push(
|
||||||
),
|
context,
|
||||||
Expanded(
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false",
|
||||||
child: GestureDetector(
|
replace: false,
|
||||||
child: Stack(
|
);
|
||||||
alignment: AlignmentDirectional.center,
|
},
|
||||||
children: [
|
),
|
||||||
PositionedDirectional(
|
],
|
||||||
start: 26.w,
|
|
||||||
top: 66.w,
|
|
||||||
child: head(url: charmTwoAvatar, width: 30.w),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 38.w,
|
|
||||||
child: head(url: charmOneAvatar, width: 37.w),
|
|
||||||
),
|
|
||||||
PositionedDirectional(
|
|
||||||
end: 25.w,
|
|
||||||
top: 66.w,
|
|
||||||
child: head(url: charmThreeAvatar, width: 30.w),
|
|
||||||
),
|
|
||||||
Image.asset(
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getPopularLeaderboardBackgroundImage('charm'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
SCNavigatorUtils.push(
|
|
||||||
context,
|
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false",
|
|
||||||
replace: false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -693,6 +667,221 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildLargeLeaderboardCard({
|
||||||
|
required String title,
|
||||||
|
required Color borderColor,
|
||||||
|
required List<Color> gradientColors,
|
||||||
|
required String imageAsset,
|
||||||
|
required List<String> avatars,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 172.w,
|
||||||
|
height: 130.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
|
border: Border.all(color: borderColor, width: 1.w),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: gradientColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: -6.w,
|
||||||
|
width: 172.w,
|
||||||
|
height: 172.w,
|
||||||
|
child: Image.asset(imageAsset, fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
_buildLeaderboardTitle(title, borderColor),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 25.w,
|
||||||
|
bottom: 6.w,
|
||||||
|
width: 123.w,
|
||||||
|
height: 16.w,
|
||||||
|
child: SvgPicture.asset(_rankRibbonsAsset, fit: BoxFit.fill),
|
||||||
|
),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 8.w,
|
||||||
|
bottom: 12.w,
|
||||||
|
child: _buildLeaderboardAvatar(
|
||||||
|
avatar: avatars[0],
|
||||||
|
size: 45.w,
|
||||||
|
avatarSize: 43.w,
|
||||||
|
frameAsset: _rankFrameSilverAsset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 20.w,
|
||||||
|
child: Center(
|
||||||
|
child: _buildLeaderboardAvatar(
|
||||||
|
avatar: avatars[1],
|
||||||
|
size: 55.w,
|
||||||
|
avatarSize: 50.w,
|
||||||
|
frameAsset: _rankFrameGoldAsset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PositionedDirectional(
|
||||||
|
end: 9.w,
|
||||||
|
bottom: 12.w,
|
||||||
|
child: _buildLeaderboardAvatar(
|
||||||
|
avatar: avatars[2],
|
||||||
|
size: 45.w,
|
||||||
|
avatarSize: 43.w,
|
||||||
|
frameAsset: _rankFrameBronzeAsset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSmallLeaderboardCard({
|
||||||
|
required String title,
|
||||||
|
required Color borderColor,
|
||||||
|
required List<Color> gradientColors,
|
||||||
|
required String imageAsset,
|
||||||
|
required double imageTop,
|
||||||
|
required double imageEnd,
|
||||||
|
required List<String> avatars,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 60.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
|
border: Border.all(color: borderColor, width: 1.w),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: gradientColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
PositionedDirectional(
|
||||||
|
end: imageEnd.w,
|
||||||
|
top: imageTop.w,
|
||||||
|
width: 66.w,
|
||||||
|
height: 66.w,
|
||||||
|
child: Image.asset(imageAsset, fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
_buildLeaderboardTitle(title, borderColor),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 10.w,
|
||||||
|
bottom: 6.w,
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(avatars.length, (index) {
|
||||||
|
const frameAssets = [
|
||||||
|
_rankFrameSilverAsset,
|
||||||
|
_rankFrameGoldAsset,
|
||||||
|
_rankFrameBronzeAsset,
|
||||||
|
];
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(
|
||||||
|
end: index == avatars.length - 1 ? 0 : 3.2.w,
|
||||||
|
),
|
||||||
|
child: _buildLeaderboardAvatar(
|
||||||
|
avatar: avatars[index],
|
||||||
|
size: 24.w,
|
||||||
|
avatarSize: 22.w,
|
||||||
|
frameAsset: frameAssets[index],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLeaderboardAvatar({
|
||||||
|
required String avatar,
|
||||||
|
required double size,
|
||||||
|
required double avatarSize,
|
||||||
|
required String frameAsset,
|
||||||
|
}) {
|
||||||
|
final url = avatar.trim();
|
||||||
|
final avatarWidget =
|
||||||
|
url.isEmpty
|
||||||
|
? ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'sc_images/general/sc_icon_avar_defalt.png',
|
||||||
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: head(
|
||||||
|
url: url,
|
||||||
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
gifFit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
avatarWidget,
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Image.asset(frameAsset, fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLeaderboardTitle(String title, Color color) {
|
||||||
|
return PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
height: 20.w,
|
||||||
|
constraints: BoxConstraints(minWidth: 70.w, maxWidth: 90.w),
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 10.w, end: 14.w),
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadiusDirectional.only(
|
||||||
|
bottomEnd: Radius.circular(8.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: const Color(0xFFDEFFF7),
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildRoomsSection() {
|
Widget _buildRoomsSection() {
|
||||||
if (!_hasCompletedInitialRoomLoad && isLoading && rooms.isEmpty) {
|
if (!_hasCompletedInitialRoomLoad && isLoading && rooms.isEmpty) {
|
||||||
return _buildRoomGridSkeleton();
|
return _buildRoomGridSkeleton();
|
||||||
@ -730,9 +919,9 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
childAspectRatio: 1,
|
childAspectRatio: 172 / 200,
|
||||||
mainAxisSpacing: 10,
|
mainAxisSpacing: 8.w,
|
||||||
crossAxisSpacing: 10,
|
crossAxisSpacing: 10.w,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.fromLTRB(10.w, 8.w, 10.w, 10.w),
|
padding: EdgeInsets.fromLTRB(10.w, 8.w, 10.w, 10.w),
|
||||||
itemCount: visibleRooms.length,
|
itemCount: visibleRooms.length,
|
||||||
@ -751,15 +940,15 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 8.w),
|
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 26.w,
|
height: 28.w,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
padding: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
||||||
itemCount: countryOptions.length + 1,
|
itemCount: countryOptions.length + 1,
|
||||||
separatorBuilder: (context, index) => SizedBox(width: 5.w),
|
separatorBuilder: (context, index) => SizedBox(width: 8.w),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return _buildCountryFilterChip(
|
return _buildCountryFilterChip(
|
||||||
@ -811,37 +1000,26 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
height: 24.w,
|
height: 28.w,
|
||||||
constraints: BoxConstraints(minWidth: 45.w, maxWidth: 95.w),
|
constraints: BoxConstraints(minWidth: countryKey == null ? 63.w : 0),
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
color:
|
||||||
begin: Alignment.topCenter,
|
isSelected
|
||||||
end: Alignment.bottomCenter,
|
? Colors.transparent
|
||||||
colors:
|
: const Color(0xFF06392C).withValues(alpha: 0.86),
|
||||||
isSelected
|
|
||||||
? const [Color(0xFF5C4430), Color(0xFF231B14)]
|
|
||||||
: [
|
|
||||||
Colors.black.withValues(alpha: 0.34),
|
|
||||||
const Color(0xFF0F3730).withValues(alpha: 0.38),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(999.w),
|
borderRadius: BorderRadius.circular(999.w),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color: isSelected ? _kPartyAccent : Colors.transparent,
|
||||||
isSelected
|
|
||||||
? const Color(0xFFE8C681)
|
|
||||||
: const Color(0x66D8B57B),
|
|
||||||
width: 1.w,
|
width: 1.w,
|
||||||
),
|
),
|
||||||
boxShadow:
|
boxShadow:
|
||||||
isSelected
|
isSelected
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFFE8C681).withValues(alpha: 0.16),
|
color: _kPartyAccent.withValues(alpha: 0.18),
|
||||||
blurRadius: 5.w,
|
blurRadius: 6.w,
|
||||||
offset: Offset(0, 2.w),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
@ -869,13 +1047,10 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color:
|
color: _kPartyAccent,
|
||||||
isSelected
|
fontSize: 12.sp,
|
||||||
? const Color(0xFFFFE4A3)
|
fontWeight: FontWeight.w500,
|
||||||
: Colors.white.withValues(alpha: 0.92),
|
height: 1,
|
||||||
fontSize: 8.sp,
|
|
||||||
fontWeight:
|
|
||||||
isSelected ? FontWeight.w700 : FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1267,161 +1442,113 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildItem(SocialChatRoomRes res, int index) {
|
_buildItem(SocialChatRoomRes res, int index) {
|
||||||
final rankBorderAsset =
|
|
||||||
index < _topRankBorderAssets.length
|
|
||||||
? _topRankBorderAssets[index]
|
|
||||||
: null;
|
|
||||||
final rankBorderOverflow = 12.w;
|
|
||||||
final rankInfoHorizontalInset = rankBorderAsset != null ? 10.w : 0.w;
|
|
||||||
final rankInfoBottomInset = rankBorderAsset != null ? 9.w : 0.w;
|
|
||||||
return SCDebounceWidget(
|
return SCDebounceWidget(
|
||||||
child: Container(
|
child: Column(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
AspectRatio(
|
||||||
color: Colors.transparent,
|
aspectRatio: 1,
|
||||||
),
|
child: Container(
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(3.w),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
color: const Color(0xFF222222),
|
||||||
image: AssetImage("sc_images/index/sc_icon_room_bord.png"),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: netImage(
|
|
||||||
url: resolveRoomCoverUrl(res.id, res.roomCover),
|
|
||||||
defaultImg: kRoomCoverDefaultImg,
|
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
width: 200.w,
|
border: Border.all(color: _kPartyAccent.withAlpha(51)),
|
||||||
height: 200.w,
|
|
||||||
),
|
),
|
||||||
),
|
clipBehavior: Clip.antiAlias,
|
||||||
Padding(
|
child: ClipRRect(
|
||||||
padding: EdgeInsets.only(
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
left: rankInfoHorizontalInset,
|
child: Stack(
|
||||||
right: rankInfoHorizontalInset,
|
|
||||||
bottom: rankInfoBottomInset,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 6.w),
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
"sc_images/index/sc_icon_index_room_brd.png",
|
|
||||||
),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: 10.w),
|
Positioned.fill(
|
||||||
Consumer<SCAppGeneralManager>(
|
child: netImage(
|
||||||
builder: (_, provider, __) {
|
url: resolveRoomCoverUrl(res.id, res.roomCover),
|
||||||
return netImage(
|
defaultImg: kRoomCoverDefaultImg,
|
||||||
url:
|
fit: BoxFit.cover,
|
||||||
"${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}",
|
|
||||||
width: 20.w,
|
|
||||||
height: 13.w,
|
|
||||||
borderRadius: BorderRadius.circular(2.w),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 17.w,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, -0.6.w),
|
|
||||||
child: text(
|
|
||||||
res.roomName ?? "",
|
|
||||||
fontSize: 13.sp,
|
|
||||||
textColor: Color(0xffffffff),
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
lineHeight: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// (roomRes.roomProfile?.roomName?.length ?? 0) > 10
|
|
||||||
// ? Marquee(
|
|
||||||
// text: roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// scrollAxis: Axis.horizontal,
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// blankSpace: 20.0,
|
|
||||||
// velocity: 40.0,
|
|
||||||
// pauseAfterRound: Duration(seconds: 1),
|
|
||||||
// accelerationDuration: Duration(seconds: 1),
|
|
||||||
// accelerationCurve: Curves.easeOut,
|
|
||||||
// decelerationDuration: Duration(
|
|
||||||
// milliseconds: 500,
|
|
||||||
// ),
|
|
||||||
// decelerationCurve: Curves.easeOut,
|
|
||||||
// )
|
|
||||||
// : Text(
|
|
||||||
// roomRes.roomProfile?.roomName ?? "",
|
|
||||||
// maxLines: 1,
|
|
||||||
// overflow: TextOverflow.ellipsis,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 15.sp,
|
|
||||||
// color: Color(0xffffffff),
|
|
||||||
// fontWeight: FontWeight.w400,
|
|
||||||
// decoration: TextDecoration.none,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 5.w),
|
Positioned.fill(
|
||||||
res.extValues?.existsPassword != true
|
child: DecoratedBox(
|
||||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
decoration: BoxDecoration(
|
||||||
: Image.asset(
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
"sc_images/index/sc_icon_room_suo.png",
|
border: Border.all(
|
||||||
width: 20.w,
|
color: _kPartyAccent.withValues(alpha: 0.2),
|
||||||
height: 20.w,
|
width: 1.w,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
res.extValues?.existsPassword != true
|
),
|
||||||
? SizedBox(width: 3.w)
|
),
|
||||||
: Container(height: 10.w),
|
PositionedDirectional(
|
||||||
res.extValues?.existsPassword != true
|
start: 8.w,
|
||||||
? text(
|
bottom: 9.w,
|
||||||
res.displayMemberCount,
|
child:
|
||||||
fontSize: 10.sp,
|
res.extValues?.existsPassword != true
|
||||||
lineHeight: 1,
|
? Row(
|
||||||
)
|
children: [
|
||||||
: Container(height: 10.w),
|
SCRoomLiveAudioIndicator(
|
||||||
SizedBox(width: 10.w),
|
width: 14.w,
|
||||||
|
height: 14.w,
|
||||||
|
),
|
||||||
|
SizedBox(width: 3.w),
|
||||||
|
text(
|
||||||
|
res.displayMemberCount,
|
||||||
|
fontSize: 10.sp,
|
||||||
|
lineHeight: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Image.asset(
|
||||||
|
"sc_images/index/sc_icon_room_suo.png",
|
||||||
|
width: 20.w,
|
||||||
|
height: 20.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (rankBorderAsset != null)
|
),
|
||||||
Positioned.fill(
|
SizedBox(height: 6.w),
|
||||||
top: -rankBorderOverflow,
|
Row(
|
||||||
left: -rankBorderOverflow,
|
children: [
|
||||||
right: -rankBorderOverflow,
|
Consumer<SCAppGeneralManager>(
|
||||||
bottom: -rankBorderOverflow,
|
builder: (_, provider, __) {
|
||||||
child: SCSvgaAssetWidget(
|
final flagUrl =
|
||||||
assetPath: rankBorderAsset,
|
_trimToNull(res.nationalFlag) ??
|
||||||
active: true,
|
_trimToNull(
|
||||||
loop: true,
|
provider
|
||||||
fit: BoxFit.fill,
|
.findCountryByName(res.countryName ?? "")
|
||||||
allowDrawingOverflow: true,
|
?.nationalFlag,
|
||||||
|
);
|
||||||
|
if (flagUrl == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: 4.w),
|
||||||
|
child: netImage(
|
||||||
|
url: flagUrl,
|
||||||
|
width: 22.w,
|
||||||
|
height: 16.w,
|
||||||
|
borderRadius: BorderRadius.circular(3.w),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(res.roomName ?? "").toUpperCase(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 16 / 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
|
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
|
||||||
|
|||||||
@ -15,13 +15,13 @@ import 'package:yumi/services/audio/rtm_manager.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.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/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
|
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_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_heartbeat_utils.dart';
|
||||||
import '../../shared/tools/sc_entry_popup_coordinator.dart';
|
import '../../shared/tools/sc_entry_popup_coordinator.dart';
|
||||||
import '../../shared/tools/sc_lk_event_bus.dart';
|
import '../../shared/tools/sc_lk_event_bus.dart';
|
||||||
@ -175,7 +175,8 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
if (!mounted && visible) {
|
if (!mounted && visible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
OverlayManager().setHomeRootTabsVisible(visible);
|
final tabAllowsFloating = _currentIndex == 0 || _currentIndex == 2;
|
||||||
|
OverlayManager().setHomeRootTabsVisible(visible && tabAllowsFloating);
|
||||||
if (visible) {
|
if (visible) {
|
||||||
unawaited(
|
unawaited(
|
||||||
Provider.of<RtmProvider>(
|
Provider.of<RtmProvider>(
|
||||||
@ -201,58 +202,46 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
const Positioned.fill(child: SCHomeShellBackground()),
|
||||||
"sc_images/index/sc_icon_index_bg.png",
|
Scaffold(
|
||||||
width: ScreenUtil().screenWidth,
|
resizeToAvoidBottomInset: false,
|
||||||
height: ScreenUtil().screenHeight,
|
backgroundColor: Colors.transparent,
|
||||||
fit: BoxFit.fill,
|
body: _buildCachedBody(),
|
||||||
),
|
bottomNavigationBar: Builder(
|
||||||
SafeArea(
|
builder: (context) {
|
||||||
top: false,
|
final bottomInset = MediaQuery.viewPaddingOf(context).bottom;
|
||||||
child: Scaffold(
|
return Container(
|
||||||
resizeToAvoidBottomInset: false,
|
height: 72.w + bottomInset,
|
||||||
backgroundColor: Colors.transparent,
|
decoration: BoxDecoration(
|
||||||
body: _buildCachedBody(),
|
color: const Color(0xFF06291F).withValues(alpha: 0.96),
|
||||||
bottomNavigationBar: Container(
|
borderRadius: BorderRadius.vertical(
|
||||||
height: 85.w,
|
top: Radius.circular(10.w),
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(
|
|
||||||
"sc_images/index/sc_index_bottom_navigation_bar_bg.png",
|
|
||||||
),
|
),
|
||||||
fit: BoxFit.fill,
|
border: Border(
|
||||||
),
|
top: BorderSide(
|
||||||
),
|
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||||
child: Theme(
|
width: 1.w,
|
||||||
data: Theme.of(context).copyWith(
|
),
|
||||||
splashFactory: NoSplash.splashFactory,
|
left: BorderSide(
|
||||||
splashColor: Colors.transparent,
|
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||||
highlightColor: Colors.transparent,
|
width: 1.w,
|
||||||
hoverColor: Colors.transparent,
|
),
|
||||||
),
|
right: BorderSide(
|
||||||
child: BottomNavigationBar(
|
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||||
elevation: 0,
|
width: 1.w,
|
||||||
enableFeedback: false,
|
),
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
selectedLabelStyle: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14.sp,
|
|
||||||
),
|
),
|
||||||
unselectedLabelStyle: TextStyle(
|
boxShadow: [
|
||||||
fontWeight: FontWeight.w500,
|
BoxShadow(
|
||||||
fontSize: 13.sp,
|
color: Colors.black.withValues(alpha: 0.26),
|
||||||
),
|
blurRadius: 18.w,
|
||||||
type: BottomNavigationBarType.fixed,
|
offset: Offset(0, -6.w),
|
||||||
selectedItemColor: Color(0xffBF854A),
|
),
|
||||||
unselectedItemColor: Color(0xffC4C4C4),
|
],
|
||||||
showUnselectedLabels: true,
|
|
||||||
showSelectedLabels: true,
|
|
||||||
items: _bottomItems,
|
|
||||||
currentIndex: _currentIndex,
|
|
||||||
onTap: _switchBottomTab,
|
|
||||||
),
|
),
|
||||||
),
|
child: _buildCustomBottomNavigation(bottomInset),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -307,6 +296,79 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomBottomNavigation(double bottomInset) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
splashFactory: NoSplash.splashFactory,
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
top: 0,
|
||||||
|
height: 72.w,
|
||||||
|
child: Row(
|
||||||
|
children: List<Widget>.generate(_bottomItems.length, (index) {
|
||||||
|
return Expanded(child: _buildCustomBottomTab(index));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: bottomInset,
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomBottomTab(int index) {
|
||||||
|
final item = _bottomItems[index];
|
||||||
|
final active = index == _currentIndex;
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => _switchBottomTab(index),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 72.w,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
Positioned(top: 0, child: active ? item.activeIcon : item.icon),
|
||||||
|
PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
bottom: 4.w,
|
||||||
|
child: Text(
|
||||||
|
item.label ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
active
|
||||||
|
? scHomeShellGoldColor
|
||||||
|
: Colors.white.withValues(alpha: 0.84),
|
||||||
|
fontSize: active ? 14.sp : 10.sp,
|
||||||
|
fontWeight: active ? FontWeight.w600 : FontWeight.w500,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _switchBottomTab(int index) {
|
void _switchBottomTab(int index) {
|
||||||
if (AccountStorage().isGuestMode && index != 0) {
|
if (AccountStorage().isGuestMode && index != 0) {
|
||||||
// 任务、消息和“我的”均为账号能力,游客点击时再引导登录。
|
// 任务、消息和“我的”均为账号能力,游客点击时再引导登录。
|
||||||
@ -324,6 +386,7 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
_currentIndexNotifier.value = index;
|
_currentIndexNotifier.value = index;
|
||||||
_builtPageIndexes.add(index);
|
_builtPageIndexes.add(index);
|
||||||
});
|
});
|
||||||
|
_setHomeRootTabsVisible(_routeObserverRoute?.isCurrent ?? true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _rebuildBottomItemsIfNeeded() {
|
void _rebuildBottomItemsIfNeeded() {
|
||||||
@ -338,13 +401,11 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: _buildBottomTabIcon(
|
icon: _buildBottomTabIcon(
|
||||||
active: false,
|
active: false,
|
||||||
svgaPath: "sc_images/index/sc_icon_home_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_room_off.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_home_no.png",
|
|
||||||
),
|
),
|
||||||
activeIcon: _buildBottomTabIcon(
|
activeIcon: _buildBottomTabIcon(
|
||||||
active: true,
|
active: true,
|
||||||
svgaPath: "sc_images/index/sc_icon_home_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_room_on.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_home_en.png",
|
|
||||||
),
|
),
|
||||||
label: SCAppLocalizations.of(context)!.home,
|
label: SCAppLocalizations.of(context)!.home,
|
||||||
),
|
),
|
||||||
@ -375,14 +436,12 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
child: _buildBottomTabIcon(
|
child: _buildBottomTabIcon(
|
||||||
active: false,
|
active: false,
|
||||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_chat_off.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_message_no.png",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _buildBottomTabIcon(
|
: _buildBottomTabIcon(
|
||||||
active: false,
|
active: false,
|
||||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_chat_off.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_message_no.png",
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -402,14 +461,12 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
child: _buildBottomTabIcon(
|
child: _buildBottomTabIcon(
|
||||||
active: true,
|
active: true,
|
||||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_chat_on.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_message_en.png",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _buildBottomTabIcon(
|
: _buildBottomTabIcon(
|
||||||
active: true,
|
active: true,
|
||||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_chat_on.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_message_en.png",
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -420,13 +477,11 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: _buildBottomTabIcon(
|
icon: _buildBottomTabIcon(
|
||||||
active: false,
|
active: false,
|
||||||
svgaPath: "sc_images/index/sc_icon_me_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_me_off.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_me_no.png",
|
|
||||||
),
|
),
|
||||||
activeIcon: _buildBottomTabIcon(
|
activeIcon: _buildBottomTabIcon(
|
||||||
active: true,
|
active: true,
|
||||||
svgaPath: "sc_images/index/sc_icon_me_anim.svga",
|
fallbackPath: "sc_images/index/sc_tab_me_on.png",
|
||||||
fallbackPath: "sc_images/index/sc_icon_me_en.png",
|
|
||||||
),
|
),
|
||||||
label: SCAppLocalizations.of(context)!.me,
|
label: SCAppLocalizations.of(context)!.me,
|
||||||
),
|
),
|
||||||
@ -614,16 +669,19 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
|
|
||||||
Widget _buildBottomTabIcon({
|
Widget _buildBottomTabIcon({
|
||||||
required bool active,
|
required bool active,
|
||||||
required String svgaPath,
|
|
||||||
required String fallbackPath,
|
required String fallbackPath,
|
||||||
}) {
|
}) {
|
||||||
return SCSvgaAssetWidget(
|
return SizedBox(
|
||||||
assetPath: svgaPath,
|
width: 50.w,
|
||||||
width: 35.w,
|
height: 50.w,
|
||||||
height: 35.w,
|
child: Center(
|
||||||
active: active,
|
child: Image.asset(
|
||||||
loop: false,
|
fallbackPath,
|
||||||
fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w),
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,11 +691,10 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
builder: (_, claimableCount, __) {
|
builder: (_, claimableCount, __) {
|
||||||
final icon = _buildBottomTabIcon(
|
final icon = _buildBottomTabIcon(
|
||||||
active: active,
|
active: active,
|
||||||
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
|
|
||||||
fallbackPath:
|
fallbackPath:
|
||||||
active
|
active
|
||||||
? "sc_images/index/sc_icon_explore_en.png"
|
? "sc_images/index/sc_tab_task_on.png"
|
||||||
: "sc_images/index/sc_icon_explore_no.png",
|
: "sc_images/index/sc_tab_task_off.png",
|
||||||
);
|
);
|
||||||
if (claimableCount <= 0) {
|
if (claimableCount <= 0) {
|
||||||
return icon;
|
return icon;
|
||||||
|
|||||||
@ -211,6 +211,7 @@ class SCMainRoute implements SCIRouterProvider {
|
|||||||
title: params['title']?.first ?? "",
|
title: params['title']?.first ?? "",
|
||||||
url: decodedUrl,
|
url: decodedUrl,
|
||||||
showTitle: params['showTitle']?.first ?? "",
|
showTitle: params['showTitle']?.first ?? "",
|
||||||
|
safeTop: params['safeTop']?.first == "true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -138,7 +138,7 @@ class _AllChatPageState extends State<AllChatPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||||
style: TextStyle(fontSize: 10.sp),
|
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||||
),
|
),
|
||||||
SizedBox(width: 4.w),
|
SizedBox(width: 4.w),
|
||||||
Icon(Icons.chevron_right, size: 10.w),
|
Icon(Icons.chevron_right, size: 10.w),
|
||||||
|
|||||||
@ -137,7 +137,7 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||||
style: TextStyle(fontSize: 10.sp),
|
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||||
),
|
),
|
||||||
SizedBox(width: 4.w),
|
SizedBox(width: 4.w),
|
||||||
Icon(Icons.chevron_right, size: 10.w),
|
Icon(Icons.chevron_right, size: 10.w),
|
||||||
|
|||||||
@ -130,7 +130,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||||
style: TextStyle(fontSize: 10.sp),
|
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||||
),
|
),
|
||||||
SizedBox(width: 4.w),
|
SizedBox(width: 4.w),
|
||||||
Icon(Icons.chevron_right, size: 10.w),
|
Icon(Icons.chevron_right, size: 10.w),
|
||||||
|
|||||||
@ -619,15 +619,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
|||||||
if (path.isEmpty) {
|
if (path.isEmpty) {
|
||||||
return SizedBox(width: width, height: width);
|
return SizedBox(width: width, height: width);
|
||||||
}
|
}
|
||||||
if (path.startsWith("assets/") || path.startsWith("sc_images/")) {
|
return RoomEmojiAssetImage(
|
||||||
return RoomEmojiAssetImage(
|
key: ValueKey(path),
|
||||||
key: ValueKey(path),
|
asset: path,
|
||||||
asset: path,
|
width: width,
|
||||||
width: width,
|
height: width,
|
||||||
height: width,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
return netImage(url: path, width: width);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkStart() {
|
void _checkStart() {
|
||||||
|
|||||||
@ -175,6 +175,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void _handleRoomRouteVisible() {
|
void _handleRoomRouteVisible() {
|
||||||
_releaseCoveredRouteGuard();
|
_releaseCoveredRouteGuard();
|
||||||
_setRoomRouteVisible(true);
|
_setRoomRouteVisible(true);
|
||||||
|
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleRoomRouteVisible() {
|
void _scheduleRoomRouteVisible() {
|
||||||
@ -253,9 +254,15 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
|
|
||||||
void _ensureRoomVisualEffectsEnabled() {
|
void _ensureRoomVisualEffectsEnabled() {
|
||||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||||
if (rtcProvider.currenRoom == null ||
|
if (rtcProvider.currenRoom == null || !_roomRouteVisible) {
|
||||||
!_roomRouteVisible ||
|
return;
|
||||||
rtcProvider.roomVisualEffectsEnabled) {
|
}
|
||||||
|
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
|
final needsListenerRebind =
|
||||||
|
rtmProvider.msgFloatingGiftListener != _floatingGiftListener ||
|
||||||
|
rtmProvider.msgLuckyGiftRewardTickerListener !=
|
||||||
|
_luckyGiftRewardTickerListener;
|
||||||
|
if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_enableRoomVisualEffects();
|
_enableRoomVisualEffects();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
@ -13,6 +14,7 @@ import 'package:yumi/modules/user/settings/settings_route.dart';
|
|||||||
import 'package:yumi/modules/user/vip/vip_route.dart';
|
import 'package:yumi/modules/user/vip/vip_route.dart';
|
||||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||||
@ -23,7 +25,7 @@ import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
|
|||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
|
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
|
|
||||||
class MePage2 extends StatefulWidget {
|
class MePage2 extends StatefulWidget {
|
||||||
const MePage2({super.key, this.currentIndexListenable, this.tabIndex = 3});
|
const MePage2({super.key, this.currentIndexListenable, this.tabIndex = 3});
|
||||||
@ -41,7 +43,6 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
'https://h5.global-interaction.com/activity/cp-space/yumi.html';
|
'https://h5.global-interaction.com/activity/cp-space/yumi.html';
|
||||||
static const String _inviteActivityUrl =
|
static const String _inviteActivityUrl =
|
||||||
'https://h5.haiyihy.com/app-invite/index.html';
|
'https://h5.haiyihy.com/app-invite/index.html';
|
||||||
static const double _inviteActivityHorizontalScale = 750 / 662;
|
|
||||||
|
|
||||||
Map<String, SCUserCounterRes> _counterMap = {};
|
Map<String, SCUserCounterRes> _counterMap = {};
|
||||||
SCVipStatusRes? _vipStatus;
|
SCVipStatusRes? _vipStatus;
|
||||||
@ -330,56 +331,79 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return Stack(
|
||||||
top: true,
|
fit: StackFit.expand,
|
||||||
bottom: false,
|
children: [
|
||||||
child: Consumer<SocialChatUserProfileManager>(
|
const Positioned.fill(child: SCHomeShellBackground()),
|
||||||
builder: (context, profileManager, child) {
|
SafeArea(
|
||||||
return SingleChildScrollView(
|
top: true,
|
||||||
physics: const BouncingScrollPhysics(),
|
bottom: false,
|
||||||
padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w),
|
child: Consumer<SocialChatUserProfileManager>(
|
||||||
child: Column(
|
builder: (context, profileManager, child) {
|
||||||
children: [
|
return SingleChildScrollView(
|
||||||
_buildProfileSection(profileManager),
|
physics: const BouncingScrollPhysics(),
|
||||||
SizedBox(height: 12.w),
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 24.w),
|
||||||
_buildStatsCard(),
|
child: Column(
|
||||||
SizedBox(height: 12.w),
|
children: [
|
||||||
_buildEntryRow(),
|
Consumer<SCAppGeneralManager>(
|
||||||
SizedBox(height: 12.w),
|
builder: (context, generalManager, child) {
|
||||||
_buildWalletVipRow(profileManager),
|
final profile = profileManager.currentUserProfile;
|
||||||
_buildInviteActivityBanner(),
|
final country =
|
||||||
_buildMenuCard2(),
|
generalManager.findCountryByName(
|
||||||
SizedBox(height: 12.w),
|
profile?.countryName ?? '',
|
||||||
_buildMenuCard([
|
) ??
|
||||||
_MenuRowData(
|
generalManager.findCountryByCode(
|
||||||
title: SCAppLocalizations.of(context)!.settings,
|
profile?.countryCode ?? '',
|
||||||
assetIcon: 'sc_images/index/sc_icon_settings.png',
|
);
|
||||||
onTap: () {
|
final flagUrl = country?.nationalFlag?.trim() ?? '';
|
||||||
SCNavigatorUtils.push(
|
return _buildProfileSection(
|
||||||
context,
|
profileManager,
|
||||||
SettingsRoute.settings,
|
flagUrl: flagUrl,
|
||||||
replace: false,
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
SizedBox(height: 12.w),
|
||||||
_MenuRowData(
|
_buildStatsCard(),
|
||||||
title: SCAppLocalizations.of(context)!.language,
|
SizedBox(height: 14.w),
|
||||||
assetIcon: 'sc_images/general/sc_icon_setting_language.png',
|
_buildProfileActionPanel(profileManager),
|
||||||
onTap: () {
|
SizedBox(height: 18.w),
|
||||||
SCNavigatorUtils.push(context, SCMainRoute.language);
|
_buildMenuGridCard([
|
||||||
},
|
..._identityMenuItems(profileManager),
|
||||||
),
|
_MenuRowData(
|
||||||
]),
|
title: SCAppLocalizations.of(context)!.settings,
|
||||||
],
|
assetIcon: 'sc_images/index/sc_icon_me_settings.svg',
|
||||||
),
|
onTap: () {
|
||||||
);
|
SCNavigatorUtils.push(
|
||||||
},
|
context,
|
||||||
),
|
SettingsRoute.settings,
|
||||||
|
replace: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_MenuRowData(
|
||||||
|
title: SCAppLocalizations.of(context)!.language,
|
||||||
|
assetIcon: 'sc_images/index/sc_icon_me_language.svg',
|
||||||
|
onTap: () {
|
||||||
|
SCNavigatorUtils.push(context, SCMainRoute.language);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProfileSection(SocialChatUserProfileManager profileManager) {
|
Widget _buildProfileSection(
|
||||||
|
SocialChatUserProfileManager profileManager, {
|
||||||
|
required String flagUrl,
|
||||||
|
}) {
|
||||||
final profile = profileManager.currentUserProfile;
|
final profile = profileManager.currentUserProfile;
|
||||||
|
final avatarUrl = profile?.userAvatar?.trim() ?? '';
|
||||||
|
|
||||||
return SCDebounceWidget(
|
return SCDebounceWidget(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -389,119 +413,202 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
replace: false,
|
replace: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
head(
|
||||||
width: 240.w,
|
url: avatarUrl,
|
||||||
height: 130.w,
|
width: 72.w,
|
||||||
child: Stack(
|
height: 72.w,
|
||||||
alignment: Alignment.topCenter,
|
shape: BoxShape.circle,
|
||||||
|
headdress:
|
||||||
|
avatarUrl.isEmpty ? null : profile?.getHeaddress()?.sourceUrl,
|
||||||
|
headdressCover:
|
||||||
|
avatarUrl.isEmpty ? null : profile?.getHeaddress()?.cover,
|
||||||
|
showDefault: true,
|
||||||
|
),
|
||||||
|
SizedBox(width: 24.w),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
head(
|
Row(
|
||||||
url: profile?.userAvatar ?? '',
|
children: [
|
||||||
width: 96.w,
|
if (flagUrl.isNotEmpty) ...[
|
||||||
height: 96.w,
|
netImage(
|
||||||
shape: BoxShape.circle,
|
url: flagUrl,
|
||||||
border: Border.all(color: Colors.white, width: 1),
|
width: 20.w,
|
||||||
headdress: profile?.getHeaddress()?.sourceUrl,
|
height: 14.w,
|
||||||
headdressCover: profile?.getHeaddress()?.cover,
|
fit: BoxFit.cover,
|
||||||
showDefault: true,
|
noDefaultImg: true,
|
||||||
|
),
|
||||||
|
SizedBox(width: 5.w),
|
||||||
|
],
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
profile?.userNickname ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (profile?.userSex != null) ...[
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
Container(
|
||||||
|
width: 18.w,
|
||||||
|
height: 16.w,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(
|
||||||
|
profile?.userSex == 0
|
||||||
|
? 'sc_images/login/sc_icon_sex_woman_bg.png'
|
||||||
|
: 'sc_images/login/sc_icon_sex_man_bg.png',
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: xb(profile?.userSex, height: 10.w, color: null),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.w),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 3.w),
|
||||||
|
child: SCSpecialIdBadge(
|
||||||
|
idText: profile?.getID() ?? '',
|
||||||
|
showAnimated: profile?.hasSpecialId() ?? false,
|
||||||
|
assetPath: SCSpecialIdAssets.userIdLarge,
|
||||||
|
animationWidth: 80.w,
|
||||||
|
animationHeight: 32.w,
|
||||||
|
showTextBesideAnimated: true,
|
||||||
|
animatedTextSpacing: 0,
|
||||||
|
showAnimatedGradientText:
|
||||||
|
profile?.shouldShowColoredSpecialIdText() ?? false,
|
||||||
|
animationTextStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
normalTextStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
animationFit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.w),
|
|
||||||
Text(
|
|
||||||
profile?.userNickname ?? '',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18.sp,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.w),
|
|
||||||
SCSpecialIdBadge(
|
|
||||||
idText: profile?.getID() ?? '',
|
|
||||||
showAnimated: profile?.hasSpecialId() ?? false,
|
|
||||||
assetPath: SCSpecialIdAssets.userIdLarge,
|
|
||||||
animationWidth: 80.w,
|
|
||||||
animationHeight: 32.w,
|
|
||||||
showTextBesideAnimated: true,
|
|
||||||
animatedTextSpacing: 0,
|
|
||||||
showAnimatedGradientText:
|
|
||||||
profile?.shouldShowColoredSpecialIdText() ?? false,
|
|
||||||
animationTextStyle: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
normalTextStyle: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
animationFit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatsCard() {
|
Widget _buildStatsCard() {
|
||||||
return _buildGlassCard(
|
return Row(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
|
children: [
|
||||||
radius: 8.w,
|
_buildStatItem(
|
||||||
child: Row(
|
value: _counterText('INTERVIEW'),
|
||||||
|
label: SCAppLocalizations.of(context)!.vistors,
|
||||||
|
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors),
|
||||||
|
),
|
||||||
|
_buildStatDivider(),
|
||||||
|
_buildStatItem(
|
||||||
|
value: _counterText('SUBSCRIPTION'),
|
||||||
|
label: SCAppLocalizations.of(context)!.following,
|
||||||
|
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow),
|
||||||
|
),
|
||||||
|
_buildStatDivider(),
|
||||||
|
_buildStatItem(
|
||||||
|
value: _counterText('FANS'),
|
||||||
|
label: SCAppLocalizations.of(context)!.friends,
|
||||||
|
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProfileActionPanel(SocialChatUserProfileManager profileManager) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 260.w,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem(
|
Positioned.fill(child: _buildActionPanelBackground()),
|
||||||
value: _counterText('INTERVIEW'),
|
PositionedDirectional(
|
||||||
label: SCAppLocalizations.of(context)!.vistors,
|
start: 0,
|
||||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors),
|
end: 0,
|
||||||
|
top: 0,
|
||||||
|
height: 76.w,
|
||||||
|
child: SCDebounceWidget(
|
||||||
|
onTap: _openInviteActivity,
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_buildStatDivider(),
|
PositionedDirectional(
|
||||||
_buildStatItem(
|
start: 11.w,
|
||||||
value: _counterText('SUBSCRIPTION'),
|
end: 11.w,
|
||||||
label: SCAppLocalizations.of(context)!.following,
|
top: 82.w,
|
||||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow),
|
child: _buildWalletVipRow(profileManager),
|
||||||
),
|
),
|
||||||
_buildStatDivider(),
|
PositionedDirectional(
|
||||||
_buildStatItem(
|
start: 18.w,
|
||||||
value: _counterText('FANS'),
|
end: 18.w,
|
||||||
label: SCAppLocalizations.of(context)!.friends,
|
top: 161.w,
|
||||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans),
|
child: _buildEntryRow(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildActionPanelBackground() {
|
||||||
|
return Image.asset(
|
||||||
|
'sc_images/index/sc_me_action_panel_bg.png',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(top: 52.w),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'sc_images/index/sc_me_action_panel_bg.svg',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEntryRow() {
|
Widget _buildEntryRow() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
_buildEntryItem(
|
_buildEntryItem(
|
||||||
title: SCAppLocalizations.of(context)!.store,
|
title: SCAppLocalizations.of(context)!.store,
|
||||||
iconPath: 'sc_images/index/sc_icon_shop.png',
|
iconPath: 'sc_images/index/sc_me_entry_store.png',
|
||||||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
|
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(
|
_buildEntryItem(
|
||||||
title: SCAppLocalizations.of(context)!.bag,
|
title: SCAppLocalizations.of(context)!.bag,
|
||||||
iconPath: 'sc_images/index/sc_icon_bag.png',
|
iconPath: 'sc_images/index/sc_me_entry_bag.png',
|
||||||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags),
|
onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10.w),
|
|
||||||
_buildEntryItem(
|
_buildEntryItem(
|
||||||
title: 'cp',
|
title: SCAppLocalizations.of(context)!.level,
|
||||||
iconPath: 'sc_images/index/sc_icon_cp_entry.png',
|
iconPath: 'sc_images/index/sc_me_entry_level.png',
|
||||||
iconWidth: 47,
|
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||||||
|
),
|
||||||
|
_buildEntryItem(
|
||||||
|
title: 'CP',
|
||||||
|
iconPath: 'sc_images/index/sc_me_entry_cp.png',
|
||||||
|
iconWidth: 46,
|
||||||
iconHeight: 50,
|
iconHeight: 50,
|
||||||
onTap: _openCpActivity,
|
onTap: _openCpActivity,
|
||||||
),
|
),
|
||||||
@ -526,6 +633,7 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
height: 68.w,
|
height: 68.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8.w),
|
borderRadius: BorderRadius.circular(8.w),
|
||||||
|
border: Border.all(color: scHomeShellGoldColor, width: 1.w),
|
||||||
image: const DecorationImage(
|
image: const DecorationImage(
|
||||||
image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'),
|
image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
@ -565,12 +673,12 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
child: Consumer<SocialChatUserProfileManager>(
|
child: Consumer<SocialChatUserProfileManager>(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
return Text(
|
return Text(
|
||||||
':${_balanceText(ref.myBalance)}',
|
'${_balanceText(ref.myBalance)}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
height: 1,
|
height: 1,
|
||||||
@ -780,68 +888,9 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInviteActivityBanner() {
|
List<_MenuRowData> _identityMenuItems(
|
||||||
return SCDebounceWidget(
|
SocialChatUserProfileManager userProfile,
|
||||||
onTap: _openInviteActivity,
|
) {
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final width =
|
|
||||||
constraints.maxWidth.isFinite
|
|
||||||
? constraints.maxWidth
|
|
||||||
: ScreenUtil().screenWidth - 20.w;
|
|
||||||
final height = width / 5;
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Transform(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
transform: Matrix4.diagonal3Values(
|
|
||||||
_inviteActivityHorizontalScale,
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
child: SCSvgaAssetWidget(
|
|
||||||
assetPath: _inviteActivitySvgaAsset,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
active: true,
|
|
||||||
loop: true,
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
allowDrawingOverflow: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String get _inviteActivitySvgaAsset {
|
|
||||||
switch (SCGlobalConfig.lang) {
|
|
||||||
case 'ar':
|
|
||||||
return 'sc_images/index/sc_invite_activity_rtl.svga';
|
|
||||||
case 'bn':
|
|
||||||
case 'fa':
|
|
||||||
return 'sc_images/index/sc_invite_activity_fa.svga';
|
|
||||||
case 'tr':
|
|
||||||
case 'pt':
|
|
||||||
return 'sc_images/index/sc_invite_activity_pt.svga';
|
|
||||||
default:
|
|
||||||
return 'sc_images/index/sc_invite_activity.svga';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMenuCard2() {
|
|
||||||
final userProfile = context.watch<SocialChatUserProfileManager>();
|
|
||||||
final items = <_MenuRowData>[];
|
final items = <_MenuRowData>[];
|
||||||
final hasCurrentUser = userProfile.currentUserProfile != null;
|
final hasCurrentUser = userProfile.currentUserProfile != null;
|
||||||
final isAgent = userProfile.userIdentity?.agent ?? false;
|
final isAgent = userProfile.userIdentity?.agent ?? false;
|
||||||
@ -862,11 +911,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.agentCenter,
|
title: SCAppLocalizations.of(context)!.agentCenter,
|
||||||
assetIcon: 'sc_images/index/sc_icon_agent_center.png',
|
assetIcon: 'sc_images/index/sc_icon_me_agency_center.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false",
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false&safeTop=true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -875,11 +924,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.hostCenter,
|
title: SCAppLocalizations.of(context)!.hostCenter,
|
||||||
assetIcon: 'sc_images/index/sc_icon_host_center.png',
|
assetIcon: 'sc_images/index/sc_icon_me_host_center.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false",
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false&safeTop=true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -893,11 +942,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.admin,
|
title: SCAppLocalizations.of(context)!.admin,
|
||||||
assetIcon: 'sc_images/index/sc_icon_bd_leader.png',
|
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false",
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false&safeTop=true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -923,11 +972,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.rechargeAgency,
|
title: SCAppLocalizations.of(context)!.rechargeAgency,
|
||||||
assetIcon: 'sc_images/index/sc_icon_recharge_agency.png',
|
assetIcon: 'sc_images/index/sc_icon_me_recharge_agency.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false",
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false&safeTop=true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -939,11 +988,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.adminCenter,
|
title: SCAppLocalizations.of(context)!.adminCenter,
|
||||||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false",
|
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false&safeTop=true",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -956,7 +1005,7 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
items.add(
|
items.add(
|
||||||
_MenuRowData(
|
_MenuRowData(
|
||||||
title: SCAppLocalizations.of(context)!.managerCenter,
|
title: SCAppLocalizations.of(context)!.managerCenter,
|
||||||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
assetIcon: 'sc_images/index/sc_icon_me_manager_center.svg',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
@ -970,58 +1019,73 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
'[MePage2] hide Manager Center because yumiManager=$isYumiManager',
|
'[MePage2] hide Manager Center because yumiManager=$isYumiManager',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _buildMenuCard(items);
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMenuCard(List<_MenuRowData> items) {
|
Widget _buildMenuGridCard(List<_MenuRowData> items) {
|
||||||
// 没有任何项时,不显示整个卡片
|
|
||||||
if (items.isEmpty) return const SizedBox.shrink();
|
if (items.isEmpty) return const SizedBox.shrink();
|
||||||
return _buildGlassCard(
|
return _buildGlassCard(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
|
||||||
radius: 4.w,
|
radius: 14.w,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final itemWidth = constraints.maxWidth / 4;
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
runSpacing: 18.w,
|
||||||
|
children:
|
||||||
|
items.map((item) {
|
||||||
|
return SizedBox(
|
||||||
|
width: itemWidth,
|
||||||
|
child: _buildMenuGridItem(item),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuGridItem(_MenuRowData item) {
|
||||||
|
return SCDebounceWidget(
|
||||||
|
onTap: item.onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < items.length; i++) ...[_buildMenuRow(items[i])],
|
_buildMenuGridIcon(item.assetIcon!),
|
||||||
|
SizedBox(height: 16.w),
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMenuRow(_MenuRowData item) {
|
Widget _buildMenuGridIcon(String assetIcon) {
|
||||||
return SCDebounceWidget(
|
if (assetIcon.endsWith('.svg')) {
|
||||||
onTap: item.onTap,
|
return SvgPicture.asset(
|
||||||
child: SizedBox(
|
assetIcon,
|
||||||
height: 46.w,
|
width: 28.w,
|
||||||
child: Row(
|
height: 28.w,
|
||||||
children: [
|
fit: BoxFit.contain,
|
||||||
Image.asset(
|
);
|
||||||
item.assetIcon!,
|
}
|
||||||
width: 24.w,
|
return Image.asset(
|
||||||
height: 24.w,
|
assetIcon,
|
||||||
color: Colors.white,
|
width: 28.w,
|
||||||
),
|
height: 28.w,
|
||||||
|
color: Colors.white,
|
||||||
SizedBox(width: 12.w),
|
fit: BoxFit.contain,
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
item.title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 15.sp,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
SCGlobalConfig.lang == 'ar'
|
|
||||||
? Icons.keyboard_arrow_left
|
|
||||||
: Icons.keyboard_arrow_right,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 18.w,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1037,13 +1101,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
child: SCDebounceWidget(
|
child: SCDebounceWidget(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 110.w,
|
height: 88.w,
|
||||||
decoration: BoxDecoration(
|
color: Colors.transparent,
|
||||||
image: const DecorationImage(
|
|
||||||
image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
@ -1052,8 +1111,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Image.asset(
|
||||||
iconPath,
|
iconPath,
|
||||||
width: (iconWidth ?? 54).w,
|
width: (iconWidth ?? 50).w,
|
||||||
height: (iconHeight ?? 54).w,
|
height: (iconHeight ?? 50).w,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.w),
|
SizedBox(height: 4.w),
|
||||||
@ -1064,7 +1123,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 15.sp,
|
fontSize: 15.sp,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1116,26 +1176,29 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
child: SCDebounceWidget(
|
child: SCDebounceWidget(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.w),
|
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: scHomeShellAccentColor.withValues(alpha: 0.7),
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.w),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: scHomeShellAccentColor,
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1148,7 +1211,7 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
return Container(
|
return Container(
|
||||||
width: 0.5.w,
|
width: 0.5.w,
|
||||||
height: 16.w,
|
height: 16.w,
|
||||||
color: const Color(0xFFE6E6E6).withValues(alpha: 0.8),
|
color: Colors.white.withValues(alpha: 0.32),
|
||||||
margin: EdgeInsets.symmetric(horizontal: 6.w),
|
margin: EdgeInsets.symmetric(horizontal: 6.w),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1163,13 +1226,16 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
|||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(radius),
|
borderRadius: BorderRadius.circular(radius),
|
||||||
border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w),
|
border: Border.all(
|
||||||
|
color: scHomeShellAccentColor.withValues(alpha: 0.2),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
const Color(0xFF0A7D49).withValues(alpha: 0.20),
|
const Color(0xFF111A1B).withValues(alpha: 0.62),
|
||||||
const Color(0xFF031513).withValues(alpha: 0.35),
|
const Color(0xFF2C4D42).withValues(alpha: 0.44),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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_room_repository_imp.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart';
|
import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
|
||||||
|
|
||||||
class TaskPage extends StatefulWidget {
|
class TaskPage extends StatefulWidget {
|
||||||
const TaskPage({
|
const TaskPage({
|
||||||
@ -27,7 +28,11 @@ class TaskPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _TaskPageState extends State<TaskPage>
|
class _TaskPageState extends State<TaskPage>
|
||||||
with WidgetsBindingObserver, RouteAware {
|
with WidgetsBindingObserver, RouteAware {
|
||||||
|
static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png';
|
||||||
|
static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png';
|
||||||
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
||||||
|
static const String _taskRechargeAsset =
|
||||||
|
'sc_images/index/sc_icon_task_recharge.png';
|
||||||
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
|
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
|
||||||
|
|
||||||
final SCAccountRepository _repository = SCAccountRepository();
|
final SCAccountRepository _repository = SCAccountRepository();
|
||||||
@ -94,9 +99,6 @@ class _TaskPageState extends State<TaskPage>
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sortedTasks =
|
final sortedTasks =
|
||||||
tasks..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
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(() {
|
setState(() {
|
||||||
_tasks = sortedTasks;
|
_tasks = sortedTasks;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
@ -133,88 +135,90 @@ class _TaskPageState extends State<TaskPage>
|
|||||||
final newcomerTasks = _sectionTasks(isNewcomer: true);
|
final newcomerTasks = _sectionTasks(isNewcomer: true);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF00221D),
|
backgroundColor: scHomeShellBackgroundColor,
|
||||||
body: SafeArea(
|
body: Stack(
|
||||||
bottom: false,
|
children: [
|
||||||
child: Stack(
|
const Positioned.fill(child: SCHomeShellBackground()),
|
||||||
children: [
|
SafeArea(
|
||||||
Column(
|
bottom: false,
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildTopBar(),
|
Column(
|
||||||
Expanded(
|
children: [
|
||||||
child: RefreshIndicator(
|
_buildTopBar(),
|
||||||
color: const Color(0xFF16DBB2),
|
Expanded(
|
||||||
backgroundColor: const Color(0xFF06342A),
|
child: RefreshIndicator(
|
||||||
onRefresh: _handleManualRefresh,
|
color: const Color(0xFF16DBB2),
|
||||||
child: SingleChildScrollView(
|
backgroundColor: const Color(0xFF06342A),
|
||||||
physics: const AlwaysScrollableScrollPhysics(
|
onRefresh: _handleManualRefresh,
|
||||||
parent: BouncingScrollPhysics(),
|
child: SingleChildScrollView(
|
||||||
),
|
physics: const AlwaysScrollableScrollPhysics(
|
||||||
padding: EdgeInsets.fromLTRB(10.w, 18.w, 10.w, 28.w),
|
parent: BouncingScrollPhysics(),
|
||||||
child: Column(
|
),
|
||||||
children: [
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 28.w),
|
||||||
if (_loading)
|
child: Column(
|
||||||
Padding(
|
children: [
|
||||||
padding: EdgeInsets.only(top: 120.w),
|
if (_loading)
|
||||||
child: SizedBox(
|
Padding(
|
||||||
width: 24.w,
|
padding: EdgeInsets.only(top: 120.w),
|
||||||
height: 24.w,
|
child: SizedBox(
|
||||||
child: const CircularProgressIndicator(
|
width: 24.w,
|
||||||
strokeWidth: 2,
|
height: 24.w,
|
||||||
color: Color(0xFF16DBB2),
|
child: const CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Color(0xFF16DBB2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
..._buildTaskSections(
|
||||||
|
dailyTasks,
|
||||||
|
newcomerTasks,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
else
|
),
|
||||||
..._buildTaskSections(dailyTasks, newcomerTasks),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
if (_toastMessage != null) _buildToast(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_toastMessage != null) _buildToast(),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopBar() {
|
Widget _buildTopBar() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 44.w,
|
height: 48.w,
|
||||||
child: Stack(
|
child: Row(
|
||||||
alignment: Alignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Align(
|
if (widget.showBackButton)
|
||||||
alignment: AlignmentDirectional.centerStart,
|
GestureDetector(
|
||||||
child:
|
behavior: HitTestBehavior.opaque,
|
||||||
widget.showBackButton
|
onTap: () => SCNavigatorUtils.goBack(context),
|
||||||
? GestureDetector(
|
child: SizedBox(
|
||||||
behavior: HitTestBehavior.opaque,
|
width: 44.w,
|
||||||
onTap: () => SCNavigatorUtils.goBack(context),
|
height: 48.w,
|
||||||
child: SizedBox(
|
child: Icon(
|
||||||
width: 44.w,
|
SCGlobalConfig.lang == 'ar'
|
||||||
height: 44.w,
|
? Icons.keyboard_arrow_right
|
||||||
child: Icon(
|
: Icons.keyboard_arrow_left,
|
||||||
SCGlobalConfig.lang == 'ar'
|
color: Colors.white,
|
||||||
? Icons.keyboard_arrow_right
|
size: 28.w,
|
||||||
: Icons.keyboard_arrow_left,
|
),
|
||||||
color: Colors.white,
|
),
|
||||||
size: 28.w,
|
)
|
||||||
),
|
else
|
||||||
),
|
SizedBox(width: 10.w),
|
||||||
)
|
SCHomeShellGradientText(
|
||||||
: SizedBox(width: 44.w, height: 44.w),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
SCAppLocalizations.of(context)!.task,
|
SCAppLocalizations.of(context)!.task,
|
||||||
style: TextStyle(
|
textAlign: TextAlign.start,
|
||||||
color: Colors.white,
|
style: scHomeShellTitleStyle(fontSize: 28.sp),
|
||||||
fontSize: 20.sp,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -517,7 +521,18 @@ class _TaskPageState extends State<TaskPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _iconForTask(SCTaskListRes task) {
|
String _iconForTask(SCTaskListRes task) {
|
||||||
return _taskGiftAsset;
|
switch (task.conditionType?.trim().toUpperCase()) {
|
||||||
|
case 'MIC_DURATION_SECONDS':
|
||||||
|
return _taskMicAsset;
|
||||||
|
case 'GAME_CONSUME_GOLD':
|
||||||
|
return _taskGameAsset;
|
||||||
|
case 'GIFT_CONSUME_GOLD':
|
||||||
|
return _taskGiftAsset;
|
||||||
|
case 'RECHARGE_GOLD':
|
||||||
|
return _taskRechargeAsset;
|
||||||
|
default:
|
||||||
|
return _taskGiftAsset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _taskProgressText(
|
String _taskProgressText(
|
||||||
@ -688,15 +703,16 @@ class _TaskSection extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF052E25),
|
color: const Color(0xFF08251E).withValues(alpha: 0.56),
|
||||||
borderRadius: BorderRadius.circular(7.w),
|
borderRadius: BorderRadius.circular(8.w),
|
||||||
border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w),
|
border: Border.all(color: const Color(0xFF1FA87C), width: 0.5.w),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(12.w, 24.w, 12.w, 12.w),
|
padding: EdgeInsets.fromLTRB(12.w, 12.w, 12.w, 8.w),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _GradientTaskTitle(title)),
|
Expanded(child: _GradientTaskTitle(title)),
|
||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8.w),
|
||||||
@ -705,10 +721,10 @@ class _TaskSection extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white.withValues(alpha: 0.72),
|
||||||
fontSize: 12.sp,
|
fontSize: 10.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
height: 1,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -726,7 +742,7 @@ class _TaskSection extends StatelessWidget {
|
|||||||
child: Divider(
|
child: Divider(
|
||||||
height: 1.w,
|
height: 1.w,
|
||||||
thickness: 0.5.w,
|
thickness: 0.5.w,
|
||||||
color: const Color(0xFF295346).withValues(alpha: 0.8),
|
color: Colors.white.withValues(alpha: 0.06),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -745,73 +761,41 @@ class _GradientTaskTitle extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
fontSize: 16.sp,
|
fontSize: 16.sp,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1,
|
height: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
return LayoutBuilder(
|
return Align(
|
||||||
builder: (context, constraints) {
|
alignment: AlignmentDirectional.centerStart,
|
||||||
return SizedBox(
|
child: Row(
|
||||||
width: constraints.maxWidth,
|
mainAxisSize: MainAxisSize.min,
|
||||||
height: 23.w,
|
children: [
|
||||||
child: Stack(
|
_TaskTitleSparkle(),
|
||||||
clipBehavior: Clip.none,
|
SizedBox(width: 4.w),
|
||||||
children: [
|
Flexible(
|
||||||
PositionedDirectional(
|
child: SCHomeShellGradientText(
|
||||||
top: 3.w,
|
text,
|
||||||
start: 0,
|
maxLines: 1,
|
||||||
end: 0,
|
overflow: TextOverflow.ellipsis,
|
||||||
child: Text(
|
style: style,
|
||||||
text,
|
),
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: style.copyWith(color: const Color(0xFF157D10)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PositionedDirectional(
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: style.copyWith(
|
|
||||||
foreground:
|
|
||||||
Paint()
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = 1.w
|
|
||||||
..color = const Color(0xFF157D10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PositionedDirectional(
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
child: ShaderMask(
|
|
||||||
blendMode: BlendMode.srcIn,
|
|
||||||
shaderCallback:
|
|
||||||
(bounds) => const LinearGradient(
|
|
||||||
begin: Alignment.centerRight,
|
|
||||||
end: Alignment.centerLeft,
|
|
||||||
colors: [
|
|
||||||
Color(0xFFFFFFFF),
|
|
||||||
Color(0xFFBEFAE8),
|
|
||||||
Color(0xFFFFFFFF),
|
|
||||||
],
|
|
||||||
stops: [0.224, 0.4907, 0.7224],
|
|
||||||
).createShader(bounds),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: style.copyWith(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
SizedBox(width: 4.w),
|
||||||
},
|
_TaskTitleSparkle(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TaskTitleSparkle extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Image.asset(
|
||||||
|
'sc_images/index/sc_task_title_sparkle.png',
|
||||||
|
width: 10.w,
|
||||||
|
height: 10.w,
|
||||||
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -834,17 +818,8 @@ class _TaskRow extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 12.w),
|
||||||
Container(
|
SizedBox(width: 42.w, height: 42.w, child: _TaskIcon(task: task)),
|
||||||
width: 42.w,
|
SizedBox(width: 8.w),
|
||||||
height: 42.w,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0x30BEF9E8),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: _TaskIcon(task: task),
|
|
||||||
),
|
|
||||||
SizedBox(width: 20.w),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -861,36 +836,43 @@ class _TaskRow extends StatelessWidget {
|
|||||||
height: 1.15,
|
height: 1.15,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 3.w),
|
SizedBox(height: 4.w),
|
||||||
if (task.requirementText.isNotEmpty) ...[
|
Row(
|
||||||
Text(
|
children: [
|
||||||
task.requirementText,
|
if (task.requirementText.isNotEmpty)
|
||||||
maxLines: 1,
|
Flexible(
|
||||||
overflow: TextOverflow.ellipsis,
|
child: Text(
|
||||||
style: TextStyle(
|
task.requirementText,
|
||||||
color: Colors.white.withValues(alpha: 0.72),
|
maxLines: 1,
|
||||||
fontSize: 10.sp,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w400,
|
style: TextStyle(
|
||||||
height: 1,
|
color: Colors.white.withValues(alpha: 0.6),
|
||||||
),
|
fontSize: 10.sp,
|
||||||
),
|
fontWeight: FontWeight.w400,
|
||||||
SizedBox(height: 3.w),
|
height: 1,
|
||||||
],
|
),
|
||||||
if (task.progressText.isNotEmpty) ...[
|
),
|
||||||
Text(
|
),
|
||||||
task.progressText,
|
if (task.requirementText.isNotEmpty &&
|
||||||
maxLines: 1,
|
task.progressText.isNotEmpty)
|
||||||
overflow: TextOverflow.ellipsis,
|
SizedBox(width: 10.w),
|
||||||
style: TextStyle(
|
if (task.progressText.isNotEmpty)
|
||||||
color: Colors.white.withValues(alpha: 0.62),
|
Flexible(
|
||||||
fontSize: 10.sp,
|
child: Text(
|
||||||
fontWeight: FontWeight.w400,
|
task.progressText,
|
||||||
height: 1,
|
maxLines: 1,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
style: TextStyle(
|
||||||
SizedBox(height: 5.w),
|
color: const Color(0xFF28E883),
|
||||||
] else
|
fontSize: 10.sp,
|
||||||
SizedBox(height: 6.w),
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 7.w),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Image.asset(
|
||||||
@ -921,7 +903,7 @@ class _TaskRow extends StatelessWidget {
|
|||||||
claiming: claiming,
|
claiming: claiming,
|
||||||
onTap: () => onAction(task),
|
onTap: () => onAction(task),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 14.w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -950,16 +932,26 @@ class _TaskActionButton extends StatelessWidget {
|
|||||||
onTap: claiming ? null : onTap,
|
onTap: claiming ? null : onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: config.width.w,
|
width: config.width.w,
|
||||||
height: 20.w,
|
height: 24.w,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10.w),
|
borderRadius: BorderRadius.circular(999.w),
|
||||||
|
image:
|
||||||
|
config.assetPath == null
|
||||||
|
? null
|
||||||
|
: DecorationImage(
|
||||||
|
image: AssetImage(config.assetPath!),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
border:
|
border:
|
||||||
config.borderColor == null
|
config.assetPath != null || config.borderColor == null
|
||||||
? null
|
? null
|
||||||
: Border.all(color: config.borderColor!, width: 1.w),
|
: Border.all(color: config.borderColor!, width: 1.w),
|
||||||
gradient: config.gradient,
|
gradient: config.assetPath == null ? config.gradient : null,
|
||||||
color: config.gradient == null ? config.backgroundColor : null,
|
color:
|
||||||
|
config.assetPath == null && config.gradient == null
|
||||||
|
? config.backgroundColor
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
claiming
|
claiming
|
||||||
@ -977,7 +969,7 @@ class _TaskActionButton extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: config.textColor,
|
color: config.textColor,
|
||||||
fontSize: 13.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
@ -1000,8 +992,8 @@ class _TaskIcon extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return Image.network(
|
return Image.network(
|
||||||
iconUrl,
|
iconUrl,
|
||||||
width: 29.4.w,
|
width: 42.w,
|
||||||
height: 29.4.w,
|
height: 42.w,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (_, __, ___) => _buildAssetIcon(),
|
errorBuilder: (_, __, ___) => _buildAssetIcon(),
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
@ -1014,8 +1006,8 @@ class _TaskIcon extends StatelessWidget {
|
|||||||
Widget _buildAssetIcon() {
|
Widget _buildAssetIcon() {
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
task.iconAsset,
|
task.iconAsset,
|
||||||
width: 29.4.w,
|
width: 42.w,
|
||||||
height: 29.4.w,
|
height: 42.w,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1029,6 +1021,7 @@ class _TaskButtonConfig {
|
|||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
this.gradient,
|
this.gradient,
|
||||||
|
this.assetPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
@ -1037,6 +1030,7 @@ class _TaskButtonConfig {
|
|||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
final Gradient? gradient;
|
final Gradient? gradient;
|
||||||
|
final String? assetPath;
|
||||||
|
|
||||||
factory _TaskButtonConfig.fromStatus(
|
factory _TaskButtonConfig.fromStatus(
|
||||||
_TaskActionStatus status,
|
_TaskActionStatus status,
|
||||||
@ -1046,8 +1040,9 @@ class _TaskButtonConfig {
|
|||||||
case _TaskActionStatus.claim:
|
case _TaskActionStatus.claim:
|
||||||
return _TaskButtonConfig(
|
return _TaskButtonConfig(
|
||||||
label: l10n.claim,
|
label: l10n.claim,
|
||||||
textColor: Color(0xFF2B1500),
|
textColor: Color(0xFF221901),
|
||||||
width: 53,
|
width: 66,
|
||||||
|
assetPath: 'sc_images/index/sc_task_action_claim_bg.png',
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
@ -1058,7 +1053,8 @@ class _TaskButtonConfig {
|
|||||||
return _TaskButtonConfig(
|
return _TaskButtonConfig(
|
||||||
label: l10n.claimed,
|
label: l10n.claimed,
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
width: 58,
|
width: 66,
|
||||||
|
assetPath: 'sc_images/index/sc_task_action_claimed_bg.png',
|
||||||
backgroundColor: const Color(0xFF062E28),
|
backgroundColor: const Color(0xFF062E28),
|
||||||
borderColor: Colors.white.withValues(alpha: 0.56),
|
borderColor: Colors.white.withValues(alpha: 0.56),
|
||||||
);
|
);
|
||||||
@ -1066,7 +1062,8 @@ class _TaskButtonConfig {
|
|||||||
return _TaskButtonConfig(
|
return _TaskButtonConfig(
|
||||||
label: l10n.go,
|
label: l10n.go,
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
width: 53,
|
width: 66,
|
||||||
|
assetPath: 'sc_images/index/sc_task_action_go_bg.png',
|
||||||
backgroundColor: const Color(0xFF062E28),
|
backgroundColor: const Color(0xFF062E28),
|
||||||
borderColor: const Color(0xFF98E4C2),
|
borderColor: const Color(0xFF98E4C2),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,11 +27,13 @@ class WebViewPage extends StatefulWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final String url;
|
final String url;
|
||||||
final String showTitle;
|
final String showTitle;
|
||||||
|
final bool safeTop;
|
||||||
|
|
||||||
const WebViewPage({
|
const WebViewPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.title = "",
|
this.title = "",
|
||||||
this.showTitle = "true",
|
this.showTitle = "true",
|
||||||
|
this.safeTop = false,
|
||||||
required this.url,
|
required this.url,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -293,7 +295,7 @@ class _WebViewPageState extends State<WebViewPage> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: widget.safeTop,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
WebViewWidget(controller: _controller),
|
WebViewWidget(controller: _controller),
|
||||||
|
|||||||
@ -2557,14 +2557,41 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> releaseRtcEngineForAppTermination() async {
|
Future<void> releaseRtcEngineForAppTermination() async {
|
||||||
|
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||||
|
final shouldQuitRoom = !_currentRoomIsEntryPreview && roomId.isNotEmpty;
|
||||||
final adapter = _roomRtcEngineAdapter;
|
final adapter = _roomRtcEngineAdapter;
|
||||||
_roomRtcEngineAdapter = null;
|
_roomRtcEngineAdapter = null;
|
||||||
_rtcEnginePrewarmTask = null;
|
_rtcEnginePrewarmTask = null;
|
||||||
|
SCHeartbeatUtils.cancelTimer();
|
||||||
_resetRoomRtcTracking();
|
_resetRoomRtcTracking();
|
||||||
try {
|
|
||||||
await adapter?.release();
|
final rtcReleaseTask = () async {
|
||||||
} catch (e) {}
|
try {
|
||||||
|
await adapter?.release();
|
||||||
|
} catch (e) {}
|
||||||
|
}();
|
||||||
|
final remoteCleanupTask = () async {
|
||||||
|
if (!shouldQuitRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _retryExitNetworkRequest(
|
||||||
|
'quit room on app termination $roomId',
|
||||||
|
() async {
|
||||||
|
final didQuit = await SCAccountRepository().quitRoom(
|
||||||
|
roomId,
|
||||||
|
silentErrorToast: true,
|
||||||
|
);
|
||||||
|
if (!didQuit) {
|
||||||
|
throw StateError('quitRoom returned false');
|
||||||
|
}
|
||||||
|
await _clearPersistedRoomMarkerIfMatches(roomId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
|
||||||
|
await rtcReleaseTask;
|
||||||
await SCVoiceRoomForegroundService.stop();
|
await SCVoiceRoomForegroundService.stop();
|
||||||
|
await remoteCleanupTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleRoomRtcVoiceVolume(
|
void _handleRoomRtcVoiceVolume(
|
||||||
@ -4172,7 +4199,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
final currentPercent = _roomRocketStatusPercent(currentStatus);
|
final currentPercent = _roomRocketStatusPercent(currentStatus);
|
||||||
if (currentStatus.currentEnergy == 0 &&
|
if (currentStatus.currentEnergy == 0 &&
|
||||||
currentPercent <= 0 &&
|
currentPercent <= 0 &&
|
||||||
currentLevel >= launch.safeLevel) {
|
currentLevel > launch.safeLevel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final launchedLevel = math.max(currentLevel, launch.safeLevel);
|
final launchedLevel = math.max(currentLevel, launch.safeLevel);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:yumi/shared/tools/sc_room_utils.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_chat_bubble_message_data.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_rocket_reward_popup_ack.dart';
|
import 'package:yumi/shared/tools/sc_room_rocket_reward_popup_ack.dart';
|
||||||
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||||
@ -24,6 +25,7 @@ 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/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
|
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart';
|
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart';
|
||||||
@ -95,6 +97,159 @@ typedef OnNewGroupMessageListener =
|
|||||||
typedef OnMessageRecvC2CReadListener = Function(List<String> messageIDList);
|
typedef OnMessageRecvC2CReadListener = Function(List<String> messageIDList);
|
||||||
typedef RtmProvider = RealTimeMessagingManager;
|
typedef RtmProvider = RealTimeMessagingManager;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool shouldRefreshRoomContributionForGiftMessage({
|
||||||
|
required String? messageType,
|
||||||
|
required String? messageRoomId,
|
||||||
|
required String? messageGroupId,
|
||||||
|
required String currentRoomId,
|
||||||
|
required String currentRoomGroupId,
|
||||||
|
}) {
|
||||||
|
if (messageType != SCRoomMsgType.gift &&
|
||||||
|
messageType != SCRoomMsgType.luckGiftAnimOther) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final normalizedRoomId = currentRoomId.trim();
|
||||||
|
if (normalizedRoomId.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final normalizedCurrentGroupId = currentRoomGroupId.trim();
|
||||||
|
final normalizedMessageGroupId = messageGroupId?.trim() ?? '';
|
||||||
|
if (normalizedCurrentGroupId.isNotEmpty &&
|
||||||
|
normalizedMessageGroupId.isNotEmpty &&
|
||||||
|
normalizedMessageGroupId != normalizedCurrentGroupId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (messageType == SCRoomMsgType.gift) {
|
||||||
|
final normalizedMessageRoomId = messageRoomId?.trim() ?? '';
|
||||||
|
if (normalizedMessageRoomId.isNotEmpty &&
|
||||||
|
normalizedMessageRoomId != normalizedRoomId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool shouldSuppressRoomCpRelationNotice({
|
||||||
|
required String? messageType,
|
||||||
|
required String? noticeAction,
|
||||||
|
}) {
|
||||||
|
return messageType == SCRoomMsgType.cpSystemNotice &&
|
||||||
|
noticeAction == "RELATION_ESTABLISHED";
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
SCFloatingMessage? buildCpRelationBroadcastFloatingMessage(dynamic rawPayload) {
|
||||||
|
Map<String, dynamic> payloadMap(dynamic value) {
|
||||||
|
if (value is Map<String, dynamic>) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is Map) {
|
||||||
|
return value.map((key, item) => MapEntry(key.toString(), item));
|
||||||
|
}
|
||||||
|
if (value is String && value.trim().isNotEmpty) {
|
||||||
|
try {
|
||||||
|
return payloadMap(json.decode(value));
|
||||||
|
} catch (_) {
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstText(Iterable<dynamic> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.toString().trim() ?? '';
|
||||||
|
if (text.isNotEmpty && text.toLowerCase() != 'null') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final root = payloadMap(rawPayload);
|
||||||
|
final nested = payloadMap(root['data']);
|
||||||
|
final payload = nested.isEmpty ? root : nested;
|
||||||
|
if (payload.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = payloadMap(payload['user']);
|
||||||
|
final cpUser = payloadMap(payload['cpUser']);
|
||||||
|
final relationType = scNormalizeCpRelationType(
|
||||||
|
firstText([payload['relationType'], payload['role']]),
|
||||||
|
);
|
||||||
|
final userId = firstText([
|
||||||
|
user['userId'],
|
||||||
|
user['id'],
|
||||||
|
payload['userId'],
|
||||||
|
payload['sendUserId'],
|
||||||
|
payload['senderUserId'],
|
||||||
|
]);
|
||||||
|
final cpUserId = firstText([
|
||||||
|
cpUser['userId'],
|
||||||
|
cpUser['id'],
|
||||||
|
payload['cpUserId'],
|
||||||
|
payload['acceptUserId'],
|
||||||
|
payload['toUserId'],
|
||||||
|
]);
|
||||||
|
final userName = firstText([
|
||||||
|
user['userNickname'],
|
||||||
|
user['nickname'],
|
||||||
|
user['account'],
|
||||||
|
payload['userNickname'],
|
||||||
|
payload['nickname'],
|
||||||
|
payload['account'],
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
final cpUserName = firstText([
|
||||||
|
cpUser['userNickname'],
|
||||||
|
cpUser['nickname'],
|
||||||
|
cpUser['account'],
|
||||||
|
payload['cpUserNickname'],
|
||||||
|
payload['acceptNickname'],
|
||||||
|
payload['toUserName'],
|
||||||
|
payload['cpUserAccount'],
|
||||||
|
cpUserId,
|
||||||
|
]);
|
||||||
|
if (userId.isEmpty &&
|
||||||
|
cpUserId.isEmpty &&
|
||||||
|
userName.isEmpty &&
|
||||||
|
cpUserName.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SCFloatingMessage(
|
||||||
|
type: scCpRelationBroadcastFloatingType,
|
||||||
|
priority: 970,
|
||||||
|
roomId: firstText([payload['roomId'], payload['groupId']]),
|
||||||
|
userId: userId,
|
||||||
|
toUserId: cpUserId,
|
||||||
|
userAvatarUrl: firstText([
|
||||||
|
user['userAvatar'],
|
||||||
|
user['avatar'],
|
||||||
|
payload['userAvatar'],
|
||||||
|
]),
|
||||||
|
toUserAvatarUrl: firstText([
|
||||||
|
cpUser['userAvatar'],
|
||||||
|
cpUser['avatar'],
|
||||||
|
payload['cpUserAvatar'],
|
||||||
|
payload['acceptUserAvatar'],
|
||||||
|
payload['toUserAvatarUrl'],
|
||||||
|
]),
|
||||||
|
userName: userName,
|
||||||
|
toUserName: cpUserName,
|
||||||
|
giftUrl: scCpRelationBroadcastAsset(relationType),
|
||||||
|
giftId: "RELATION_ESTABLISHED",
|
||||||
|
giftTab: relationType,
|
||||||
|
gameId: firstText([payload['applyId'], payload['eventId']]),
|
||||||
|
displayText: firstText([payload['msg']]),
|
||||||
|
durationSeconds: 5,
|
||||||
|
broadcastScope: SCFloatingMessage.broadcastScopeRegion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
enum _CpInviteProcessResult { accepted, rejected, expired }
|
enum _CpInviteProcessResult { accepted, rejected, expired }
|
||||||
|
|
||||||
class _LuckGiftPushQueueEntry {
|
class _LuckGiftPushQueueEntry {
|
||||||
@ -217,6 +372,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
static const int _luckyGiftBurstRecentEventTtlMs = 5000;
|
static const int _luckyGiftBurstRecentEventTtlMs = 5000;
|
||||||
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
|
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
|
||||||
static const int _cpRelationGiftRefreshMinIntervalMs = 800;
|
static const int _cpRelationGiftRefreshMinIntervalMs = 800;
|
||||||
|
static const Duration _roomContributionGiftRefreshDebounce = Duration(
|
||||||
|
milliseconds: 500,
|
||||||
|
);
|
||||||
static const List<Duration> _cpRelationGiftRefreshDelays = [
|
static const List<Duration> _cpRelationGiftRefreshDelays = [
|
||||||
Duration(milliseconds: 300),
|
Duration(milliseconds: 300),
|
||||||
Duration(milliseconds: 1500),
|
Duration(milliseconds: 1500),
|
||||||
@ -358,6 +516,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
|
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
|
||||||
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
|
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
|
||||||
int _lastCpRelationGiftRefreshAt = 0;
|
int _lastCpRelationGiftRefreshAt = 0;
|
||||||
|
Timer? _roomContributionGiftRefreshTimer;
|
||||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||||
String? roomRedPacketBroadcastGroupId;
|
String? roomRedPacketBroadcastGroupId;
|
||||||
String? roomRedPacketBroadcastRegionCode;
|
String? roomRedPacketBroadcastRegionCode;
|
||||||
@ -1472,15 +1631,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result == _CpInviteProcessResult.accepted) {
|
if (result == _CpInviteProcessResult.accepted) {
|
||||||
if (isSender) {
|
|
||||||
_dispatchCpRelationEstablishedRoomNotice(
|
|
||||||
groupId: _currentVisibleVoiceRoomGroupId(context),
|
|
||||||
applyId: applyId,
|
|
||||||
data: data,
|
|
||||||
invite: invite,
|
|
||||||
currentProfile: currentProfile,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result == _CpInviteProcessResult.expired) {
|
if (result == _CpInviteProcessResult.expired) {
|
||||||
@ -1793,15 +1943,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
|
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
|
||||||
unawaited(profileManager.refreshLoadedCpProfiles());
|
unawaited(profileManager.refreshLoadedCpProfiles());
|
||||||
if (agree) {
|
if (!agree) {
|
||||||
_dispatchCpRelationEstablishedRoomNotice(
|
|
||||||
groupId: voiceRoomGroupId,
|
|
||||||
applyId: applyId,
|
|
||||||
data: data,
|
|
||||||
invite: invite,
|
|
||||||
currentProfile: AccountStorage().getCurrentUser()?.userProfile,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_dispatchCpInviteRejectedRoomNotice(
|
_dispatchCpInviteRejectedRoomNotice(
|
||||||
groupId: voiceRoomGroupId,
|
groupId: voiceRoomGroupId,
|
||||||
applyId: applyId,
|
applyId: applyId,
|
||||||
@ -1823,66 +1965,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _dispatchCpRelationEstablishedRoomNotice({
|
|
||||||
required String groupId,
|
|
||||||
required String applyId,
|
|
||||||
required SCSystemInvitMessageRes invite,
|
|
||||||
required SocialChatUserProfile? currentProfile,
|
|
||||||
Map<String, dynamic>? data,
|
|
||||||
}) {
|
|
||||||
if (currentProfile == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (groupId.isEmpty) {
|
|
||||||
_cpInviteLog(
|
|
||||||
'skip room established notice reason=no_visible_voice_room '
|
|
||||||
'applyId=$applyId',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final isSender = _cpInviteSenderMatchesCurrent(
|
|
||||||
invite,
|
|
||||||
currentProfile,
|
|
||||||
data: data,
|
|
||||||
);
|
|
||||||
final inviter = isSender ? currentProfile : _cpInviteInviterProfile(invite);
|
|
||||||
final receiver = isSender ? _cpInviteReceiverProfile(data) : currentProfile;
|
|
||||||
final relationType = scNormalizeCpRelationType(
|
|
||||||
_firstCpInviteValue([
|
|
||||||
invite.relationType,
|
|
||||||
data == null ? null : data["relationType"]?.toString(),
|
|
||||||
data == null ? null : data["relation_type"]?.toString(),
|
|
||||||
]) ??
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
final targetUserIds = _compactCpNoticeTargetIds([inviter, receiver]);
|
|
||||||
_cpInviteLog(
|
|
||||||
'dispatch room established notice applyId=$applyId '
|
|
||||||
'relationType=$relationType groupId=$groupId '
|
|
||||||
'isSender=$isSender targets=${targetUserIds.join(",")}',
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
dispatchMessage(
|
|
||||||
Msg(
|
|
||||||
groupId: groupId,
|
|
||||||
msg: scBuildCpRelationEstablishedNotice(
|
|
||||||
userName1: _roomMsgUserDisplayName(inviter),
|
|
||||||
userName2: _roomMsgUserDisplayName(receiver),
|
|
||||||
relationType: relationType,
|
|
||||||
),
|
|
||||||
type: SCRoomMsgType.cpSystemNotice,
|
|
||||||
noticeAction: "RELATION_ESTABLISHED",
|
|
||||||
user: receiver,
|
|
||||||
toUser: inviter,
|
|
||||||
targetUserIds: targetUserIds,
|
|
||||||
giftBatchId: applyId,
|
|
||||||
role: relationType,
|
|
||||||
),
|
|
||||||
addLocal: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _dispatchCpInviteRejectedRoomNotice({
|
void _dispatchCpInviteRejectedRoomNotice({
|
||||||
required String groupId,
|
required String groupId,
|
||||||
required String applyId,
|
required String applyId,
|
||||||
@ -1988,27 +2070,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SocialChatUserProfile _cpInviteReceiverProfile(Map<String, dynamic>? data) {
|
|
||||||
final receiverData =
|
|
||||||
data == null
|
|
||||||
? const <String, dynamic>{}
|
|
||||||
: Map<String, dynamic>.from(data);
|
|
||||||
final receiver = _cpInviteReceiverFromMessageData(receiverData);
|
|
||||||
final userId = _cpInviteReceiverIdFromMessageData(receiverData);
|
|
||||||
return SocialChatUserProfile(
|
|
||||||
id: userId,
|
|
||||||
account:
|
|
||||||
_firstCpInviteValue([
|
|
||||||
receiverData["acceptAccount"]?.toString(),
|
|
||||||
receiverData["receiverAccount"]?.toString(),
|
|
||||||
userId,
|
|
||||||
]) ??
|
|
||||||
userId,
|
|
||||||
userNickname: receiver.name,
|
|
||||||
userAvatar: receiver.avatarUrl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _compactCpNoticeTargetIds(
|
List<String> _compactCpNoticeTargetIds(
|
||||||
Iterable<SocialChatUserProfile?> profiles,
|
Iterable<SocialChatUserProfile?> profiles,
|
||||||
) {
|
) {
|
||||||
@ -2606,6 +2667,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
String msg,
|
String msg,
|
||||||
V2TimConversation toConversation,
|
V2TimConversation toConversation,
|
||||||
) async {
|
) async {
|
||||||
|
final chatBubbleCloudData = scEncodeChatBubbleMessageSnapshot(
|
||||||
|
AccountStorage().getCurrentUser()?.userProfile?.getChatBox(),
|
||||||
|
);
|
||||||
// 创建文本消息
|
// 创建文本消息
|
||||||
V2TimValueCallback<V2TimMsgCreateInfoResult> createTextMessageRes =
|
V2TimValueCallback<V2TimMsgCreateInfoResult> createTextMessageRes =
|
||||||
await TencentImSDKPlugin.v2TIMManager
|
await TencentImSDKPlugin.v2TIMManager
|
||||||
@ -2628,6 +2692,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
id: id, // 创建的messageid
|
id: id, // 创建的messageid
|
||||||
receiver: toConversation.userID!, // 接收人id
|
receiver: toConversation.userID!, // 接收人id
|
||||||
needReadReceipt: true,
|
needReadReceipt: true,
|
||||||
|
cloudCustomData: chatBubbleCloudData,
|
||||||
groupID: '', // 是否需要已读回执
|
groupID: '', // 是否需要已读回执
|
||||||
);
|
);
|
||||||
if (sendMessageRes.code == 0) {
|
if (sendMessageRes.code == 0) {
|
||||||
@ -2973,6 +3038,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
addMsg(Msg msg) {
|
addMsg(Msg msg) {
|
||||||
_scheduleRoomRocketStatusRefreshForGiftMessage(msg);
|
_scheduleRoomRocketStatusRefreshForGiftMessage(msg);
|
||||||
_scheduleCpRelationRefreshForGiftMessage(msg);
|
_scheduleCpRelationRefreshForGiftMessage(msg);
|
||||||
|
_scheduleRoomContributionRefreshForGiftMessage(msg);
|
||||||
final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg);
|
final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg);
|
||||||
if (mergedGiftMsg != null) {
|
if (mergedGiftMsg != null) {
|
||||||
msgAllListener?.call(mergedGiftMsg);
|
msgAllListener?.call(mergedGiftMsg);
|
||||||
@ -2997,6 +3063,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (SCRoomMsgType.isRocketRewardUserType(msg.type)) {
|
if (SCRoomMsgType.isRocketRewardUserType(msg.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (shouldSuppressRoomCpRelationNotice(
|
||||||
|
messageType: msg.type,
|
||||||
|
noticeAction: msg.noticeAction,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type == SCRoomMsgType.rocketLaunchNotice &&
|
if (msg.type == SCRoomMsgType.rocketLaunchNotice &&
|
||||||
_hasRoomRocketNoticeMessage(msg)) {
|
_hasRoomRocketNoticeMessage(msg)) {
|
||||||
return;
|
return;
|
||||||
@ -3111,6 +3183,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
giftTab: relationType,
|
giftTab: relationType,
|
||||||
displayText: msg.msg ?? "",
|
displayText: msg.msg ?? "",
|
||||||
durationSeconds: 5,
|
durationSeconds: 5,
|
||||||
|
gameId: msg.giftBatchId,
|
||||||
broadcastScope:
|
broadcastScope:
|
||||||
isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '',
|
isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '',
|
||||||
),
|
),
|
||||||
@ -3209,6 +3282,35 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showCpRelationFormedDialogFromBroadcast(SCFloatingMessage message) {
|
||||||
|
final inviter = SocialChatUserProfile(
|
||||||
|
id: message.userId,
|
||||||
|
userNickname: message.userName,
|
||||||
|
userAvatar: message.userAvatarUrl,
|
||||||
|
);
|
||||||
|
final receiver = SocialChatUserProfile(
|
||||||
|
id: message.toUserId,
|
||||||
|
userNickname: message.toUserName,
|
||||||
|
userAvatar: message.toUserAvatarUrl,
|
||||||
|
);
|
||||||
|
_showCpRelationFormedDialogIfNeeded(
|
||||||
|
Msg(
|
||||||
|
groupId: '',
|
||||||
|
msg: message.displayText,
|
||||||
|
type: SCRoomMsgType.cpSystemNotice,
|
||||||
|
noticeAction: 'RELATION_ESTABLISHED',
|
||||||
|
user: receiver,
|
||||||
|
toUser: inviter,
|
||||||
|
targetUserIds: [
|
||||||
|
if ((message.userId ?? '').trim().isNotEmpty) message.userId!,
|
||||||
|
if ((message.toUserId ?? '').trim().isNotEmpty) message.toUserId!,
|
||||||
|
],
|
||||||
|
giftBatchId: message.gameId,
|
||||||
|
role: message.giftTab,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showCpInviteRejectedDialogIfNeeded(Msg msg) {
|
void _showCpInviteRejectedDialogIfNeeded(Msg msg) {
|
||||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
if (currentProfile == null ||
|
if (currentProfile == null ||
|
||||||
@ -3355,6 +3457,43 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
rtcProvider.scheduleRoomRocketStatusRefreshForGift(roomId: currentRoomId);
|
rtcProvider.scheduleRoomRocketStatusRefreshForGift(roomId: currentRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleRoomContributionRefreshForGiftMessage(Msg msg) {
|
||||||
|
final currentContext = context;
|
||||||
|
if (currentContext == null || !currentContext.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||||
|
currentContext,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final currentRoomId =
|
||||||
|
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim();
|
||||||
|
final currentRoomGroupId =
|
||||||
|
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? '')
|
||||||
|
.trim();
|
||||||
|
if (!shouldRefreshRoomContributionForGiftMessage(
|
||||||
|
messageType: msg.type,
|
||||||
|
messageRoomId: msg.msg,
|
||||||
|
messageGroupId: msg.groupId,
|
||||||
|
currentRoomId: currentRoomId,
|
||||||
|
currentRoomGroupId: currentRoomGroupId,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_roomContributionGiftRefreshTimer?.cancel();
|
||||||
|
_roomContributionGiftRefreshTimer = Timer(
|
||||||
|
_roomContributionGiftRefreshDebounce,
|
||||||
|
() {
|
||||||
|
final latestRoomId =
|
||||||
|
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim();
|
||||||
|
if (latestRoomId != currentRoomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socialChatRoomManager.fetchContributionLevelData(currentRoomId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _scheduleCpRelationRefreshForGiftMessage(Msg msg) {
|
void _scheduleCpRelationRefreshForGiftMessage(Msg msg) {
|
||||||
if (!_isCpRelationGiftMessage(msg)) {
|
if (!_isCpRelationGiftMessage(msg)) {
|
||||||
return;
|
return;
|
||||||
@ -4196,7 +4335,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final decoded = json.decode(customData);
|
final decoded = json.decode(customData);
|
||||||
final payload = _broadcastPayloadMap(decoded);
|
final payload = _broadcastPayloadMap(decoded);
|
||||||
return payload['type'] == SCRoomMsgType.roomRedPacket;
|
return payload['type'] == SCRoomMsgType.roomRedPacket ||
|
||||||
|
payload['type'] == SCRoomMsgType.cpRelationBroadcast;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -6077,34 +6217,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (isRegionBroadcastGroup) {
|
if (isRegionBroadcastGroup) {
|
||||||
_handleRegionGiftBroadcast(data["data"]);
|
_handleRegionGiftBroadcast(data["data"]);
|
||||||
}
|
}
|
||||||
} else if (type == SCRoomMsgType.cpSystemNotice) {
|
} else if (type == SCRoomMsgType.cpRelationBroadcast) {
|
||||||
final payload = data["data"] ?? data;
|
final floatingMessage = buildCpRelationBroadcastFloatingMessage(
|
||||||
final payloadMap = _broadcastPayloadMap(payload);
|
data["data"] ?? data,
|
||||||
if (payloadMap.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final msg = Msg.fromJson(payloadMap);
|
|
||||||
msg.type ??= SCRoomMsgType.cpSystemNotice;
|
|
||||||
msg.groupId = _firstNonBlank([
|
|
||||||
msg.groupId,
|
|
||||||
_payloadText(payloadMap["groupId"]),
|
|
||||||
_payloadText(payloadMap["roomId"]),
|
|
||||||
_payloadText(data["groupId"]),
|
|
||||||
_payloadText(data["roomId"]),
|
|
||||||
]);
|
|
||||||
msg.noticeAction ??= data["noticeAction"]?.toString();
|
|
||||||
if ((msg.role ?? "").trim().isEmpty) {
|
|
||||||
msg.role = _firstNonBlank([
|
|
||||||
_payloadText(payloadMap["role"]),
|
|
||||||
_payloadText(payloadMap["relationType"]),
|
|
||||||
_payloadText(data["role"]),
|
|
||||||
_payloadText(data["relationType"]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
_enqueueCpRelationBroadcastFloatingIfNeeded(
|
|
||||||
msg,
|
|
||||||
isRegionBroadcast: isRegionBroadcastGroup,
|
|
||||||
);
|
);
|
||||||
|
if (floatingMessage != null) {
|
||||||
|
OverlayManager().addMessage(floatingMessage);
|
||||||
|
_showCpRelationFormedDialogFromBroadcast(floatingMessage);
|
||||||
|
}
|
||||||
} else if (type == SCRoomMsgType.inviteRoom) {
|
} else if (type == SCRoomMsgType.inviteRoom) {
|
||||||
///邀请进入房间
|
///邀请进入房间
|
||||||
var fdata = data["data"];
|
var fdata = data["data"];
|
||||||
@ -6820,6 +6940,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
_hiddenCurrentRoomRocketLaunchKeys.clear();
|
_hiddenCurrentRoomRocketLaunchKeys.clear();
|
||||||
_roomRocketRewardPopupRetryTimer?.cancel();
|
_roomRocketRewardPopupRetryTimer?.cancel();
|
||||||
_roomRocketRewardPopupRetryTimer = null;
|
_roomRocketRewardPopupRetryTimer = null;
|
||||||
|
_roomContributionGiftRefreshTimer?.cancel();
|
||||||
|
_roomContributionGiftRefreshTimer = null;
|
||||||
for (final timer in _roomRocketRewardHoldTimers.values) {
|
for (final timer in _roomRocketRewardHoldTimers.values) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
List<CountryMode> countryModeList = [];
|
List<CountryMode> countryModeList = [];
|
||||||
final Map<String, List<Country>> _countryMap = {};
|
final Map<String, List<Country>> _countryMap = {};
|
||||||
final Map<String, Country> _countryByNameMap = {};
|
final Map<String, Country> _countryByNameMap = {};
|
||||||
|
final Map<String, Country> _countryByCodeMap = {};
|
||||||
bool _isFetchingCountryList = false;
|
bool _isFetchingCountryList = false;
|
||||||
|
|
||||||
///礼物
|
///礼物
|
||||||
@ -114,6 +115,7 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
final result = await SCConfigRepositoryImp().loadCountry();
|
final result = await SCConfigRepositoryImp().loadCountry();
|
||||||
_countryMap.clear();
|
_countryMap.clear();
|
||||||
_countryByNameMap.clear();
|
_countryByNameMap.clear();
|
||||||
|
_countryByCodeMap.clear();
|
||||||
countryModeList.clear();
|
countryModeList.clear();
|
||||||
|
|
||||||
for (final c in result.openCountry ?? const <Country>[]) {
|
for (final c in result.openCountry ?? const <Country>[]) {
|
||||||
@ -126,6 +128,10 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
if ((c.countryName ?? "").isNotEmpty) {
|
if ((c.countryName ?? "").isNotEmpty) {
|
||||||
_countryByNameMap[c.countryName!] = c;
|
_countryByNameMap[c.countryName!] = c;
|
||||||
}
|
}
|
||||||
|
final countryCode = c.alphaTwo?.trim().toUpperCase() ?? "";
|
||||||
|
if (countryCode.isNotEmpty) {
|
||||||
|
_countryByCodeMap[countryCode] = c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_countryMap.forEach((k, v) {
|
_countryMap.forEach((k, v) {
|
||||||
@ -147,6 +153,10 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
return _countryByNameMap[countryName];
|
return _countryByNameMap[countryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Country? findCountryByCode(String countryCode) {
|
||||||
|
return _countryByCodeMap[countryCode.trim().toUpperCase()];
|
||||||
|
}
|
||||||
|
|
||||||
///选择国家
|
///选择国家
|
||||||
Country? chooseCountry(int subIndex, int cIndex) {
|
Country? chooseCountry(int subIndex, int cIndex) {
|
||||||
var selectCm = countryModeList[subIndex].prefixCountrys[cIndex];
|
var selectCm = countryModeList[subIndex].prefixCountrys[cIndex];
|
||||||
@ -468,14 +478,41 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<Emojis>> emojiByTab = {};
|
Map<String, List<Emojis>> emojiByTab = {};
|
||||||
|
List<SCRoomEmojiRes> roomEmojiGroups = [];
|
||||||
|
bool _isFetchingRoomEmojis = false;
|
||||||
|
bool _hasRoomEmojisLoaded = false;
|
||||||
|
|
||||||
|
bool get isFetchingRoomEmojis => _isFetchingRoomEmojis;
|
||||||
|
|
||||||
|
bool get hasRoomEmojisLoaded => _hasRoomEmojisLoaded;
|
||||||
|
|
||||||
///emoji表情
|
///emoji表情
|
||||||
void emojiAll() async {
|
Future<void> emojiAll() async {
|
||||||
var roomEmojis = await SCChatRoomRepository().emojiAll();
|
if (_isFetchingRoomEmojis || _hasRoomEmojisLoaded) {
|
||||||
for (var value in roomEmojis) {
|
return;
|
||||||
emojiByTab[value.groupName!] = value.emojis ?? [];
|
}
|
||||||
|
|
||||||
|
_isFetchingRoomEmojis = true;
|
||||||
|
try {
|
||||||
|
final roomEmojis = await SCChatRoomRepository().emojiAll();
|
||||||
|
roomEmojiGroups =
|
||||||
|
roomEmojis
|
||||||
|
.where((group) => (group.emojis ?? const []).isNotEmpty,)
|
||||||
|
.toList();
|
||||||
|
emojiByTab.clear();
|
||||||
|
for (final value in roomEmojiGroups) {
|
||||||
|
final groupName = value.groupName?.trim() ?? "";
|
||||||
|
if (groupName.isNotEmpty) {
|
||||||
|
emojiByTab[groupName] = value.emojis ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_hasRoomEmojisLoaded = true;
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
_isFetchingRoomEmojis = false;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///加载等级资源
|
///加载等级资源
|
||||||
|
|||||||
@ -1,146 +1,178 @@
|
|||||||
/// id : 0
|
// id : 0
|
||||||
/// sysOrigin : ""
|
// sysOrigin : ""
|
||||||
/// groupCode : ""
|
// groupCode : ""
|
||||||
/// groupName : ""
|
// groupName : ""
|
||||||
/// cover : ""
|
// cover : ""
|
||||||
/// have : false
|
// have : false
|
||||||
/// amount : 0.0
|
// amount : 0.0
|
||||||
/// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
||||||
|
|
||||||
class SCRoomEmojiRes {
|
class SCRoomEmojiRes {
|
||||||
SCRoomEmojiRes({
|
SCRoomEmojiRes({
|
||||||
String? id,
|
String? id,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
String? groupCode,
|
String? groupCode,
|
||||||
String? groupName,
|
String? groupName,
|
||||||
String? cover,
|
String? cover,
|
||||||
bool? have,
|
bool? have,
|
||||||
num? amount,
|
num? amount,
|
||||||
List<Emojis>? emojis,}){
|
List<Emojis>? emojis,}){
|
||||||
_id = id;
|
_id = id;
|
||||||
_sysOrigin = sysOrigin;
|
_sysOrigin = sysOrigin;
|
||||||
_groupCode = groupCode;
|
_groupCode = groupCode;
|
||||||
_groupName = groupName;
|
_groupName = groupName;
|
||||||
_cover = cover;
|
_cover = cover;
|
||||||
_have = have;
|
_have = have;
|
||||||
_amount = amount;
|
_amount = amount;
|
||||||
_emojis = emojis;
|
_emojis = emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
SCRoomEmojiRes.fromJson(dynamic json) {
|
SCRoomEmojiRes.fromJson(dynamic json) {
|
||||||
_id = json['id'];
|
final map = json is Map ? json : const <String, dynamic>{};
|
||||||
_sysOrigin = json['sysOrigin'];
|
_id = _stringOrNull(map['id']);
|
||||||
_groupCode = json['groupCode'];
|
_sysOrigin = _stringOrNull(map['sysOrigin']);
|
||||||
_groupName = json['groupName'];
|
_groupCode = _stringOrNull(map['groupCode']);
|
||||||
_cover = json['cover'];
|
_groupName = _stringOrNull(map['groupName']);
|
||||||
_have = json['have'];
|
_cover = _stringOrNull(map['cover']);
|
||||||
_amount = json['amount'];
|
_have = _boolOrNull(map['have']);
|
||||||
if (json['emojis'] != null) {
|
_amount = _numOrNull(map['amount']);
|
||||||
_emojis = [];
|
if (map['emojis'] is List) {
|
||||||
json['emojis'].forEach((v) {
|
_emojis = [];
|
||||||
_emojis?.add(Emojis.fromJson(v));
|
map['emojis'].forEach((v) {
|
||||||
});
|
_emojis?.add(Emojis.fromJson(v));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
String? _id;
|
}
|
||||||
String? _sysOrigin;
|
String? _id;
|
||||||
String? _groupCode;
|
String? _sysOrigin;
|
||||||
String? _groupName;
|
String? _groupCode;
|
||||||
String? _cover;
|
String? _groupName;
|
||||||
bool? _have;
|
String? _cover;
|
||||||
num? _amount;
|
bool? _have;
|
||||||
List<Emojis>? _emojis;
|
num? _amount;
|
||||||
SCRoomEmojiRes copyWith({ String? id,
|
List<Emojis>? _emojis;
|
||||||
String? sysOrigin,
|
SCRoomEmojiRes copyWith({ String? id,
|
||||||
String? groupCode,
|
String? sysOrigin,
|
||||||
String? groupName,
|
String? groupCode,
|
||||||
String? cover,
|
String? groupName,
|
||||||
bool? have,
|
String? cover,
|
||||||
num? amount,
|
bool? have,
|
||||||
List<Emojis>? emojis,
|
num? amount,
|
||||||
}) => SCRoomEmojiRes( id: id ?? _id,
|
List<Emojis>? emojis,
|
||||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
}) => SCRoomEmojiRes( id: id ?? _id,
|
||||||
groupCode: groupCode ?? _groupCode,
|
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||||
groupName: groupName ?? _groupName,
|
groupCode: groupCode ?? _groupCode,
|
||||||
cover: cover ?? _cover,
|
groupName: groupName ?? _groupName,
|
||||||
have: have ?? _have,
|
cover: cover ?? _cover,
|
||||||
amount: amount ?? _amount,
|
have: have ?? _have,
|
||||||
emojis: emojis ?? _emojis,
|
amount: amount ?? _amount,
|
||||||
);
|
emojis: emojis ?? _emojis,
|
||||||
String? get id => _id;
|
);
|
||||||
String? get sysOrigin => _sysOrigin;
|
String? get id => _id;
|
||||||
String? get groupCode => _groupCode;
|
String? get sysOrigin => _sysOrigin;
|
||||||
String? get groupName => _groupName;
|
String? get groupCode => _groupCode;
|
||||||
String? get cover => _cover;
|
String? get groupName => _groupName;
|
||||||
bool? get have => _have;
|
String? get cover => _cover;
|
||||||
num? get amount => _amount;
|
bool? get have => _have;
|
||||||
List<Emojis>? get emojis => _emojis;
|
num? get amount => _amount;
|
||||||
|
List<Emojis>? get emojis => _emojis;
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final map = <String, dynamic>{};
|
Map<String, dynamic> toJson() {
|
||||||
map['id'] = _id;
|
final map = <String, dynamic>{};
|
||||||
map['sysOrigin'] = _sysOrigin;
|
map['id'] = _id;
|
||||||
map['groupCode'] = _groupCode;
|
map['sysOrigin'] = _sysOrigin;
|
||||||
map['groupName'] = _groupName;
|
map['groupCode'] = _groupCode;
|
||||||
map['cover'] = _cover;
|
map['groupName'] = _groupName;
|
||||||
map['have'] = _have;
|
map['cover'] = _cover;
|
||||||
map['amount'] = _amount;
|
map['have'] = _have;
|
||||||
if (_emojis != null) {
|
map['amount'] = _amount;
|
||||||
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
if (_emojis != null) {
|
||||||
}
|
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
||||||
return map;
|
}
|
||||||
}
|
return map;
|
||||||
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
/// sourceUrl : ""
|
|
||||||
/// sourceType : 0
|
/// sourceUrl : ""
|
||||||
/// coverUrl : ""
|
/// sourceType : 0
|
||||||
/// type : ""
|
/// coverUrl : ""
|
||||||
|
/// type : ""
|
||||||
class Emojis {
|
|
||||||
Emojis({
|
class Emojis {
|
||||||
String? sourceUrl,
|
Emojis({
|
||||||
num? sourceType,
|
String? sourceUrl,
|
||||||
String? coverUrl,
|
num? sourceType,
|
||||||
String? type,}){
|
String? coverUrl,
|
||||||
_sourceUrl = sourceUrl;
|
String? type,}){
|
||||||
_sourceType = sourceType;
|
_sourceUrl = sourceUrl;
|
||||||
_coverUrl = coverUrl;
|
_sourceType = sourceType;
|
||||||
_type = type;
|
_coverUrl = coverUrl;
|
||||||
}
|
_type = type;
|
||||||
|
}
|
||||||
Emojis.fromJson(dynamic json) {
|
|
||||||
_sourceUrl = json['sourceUrl'];
|
Emojis.fromJson(dynamic json) {
|
||||||
_sourceType = json['sourceType'];
|
final map = json is Map ? json : const <String, dynamic>{};
|
||||||
_coverUrl = json['coverUrl'];
|
_sourceUrl = _stringOrNull(map['sourceUrl']);
|
||||||
_type = json['type'];
|
_sourceType = _numOrNull(map['sourceType']);
|
||||||
}
|
_coverUrl = _stringOrNull(map['coverUrl']);
|
||||||
String? _sourceUrl;
|
_type = _stringOrNull(map['type']);
|
||||||
num? _sourceType;
|
}
|
||||||
String? _coverUrl;
|
String? _sourceUrl;
|
||||||
String? _type;
|
num? _sourceType;
|
||||||
Emojis copyWith({ String? sourceUrl,
|
String? _coverUrl;
|
||||||
num? sourceType,
|
String? _type;
|
||||||
String? coverUrl,
|
Emojis copyWith({ String? sourceUrl,
|
||||||
String? type,
|
num? sourceType,
|
||||||
}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl,
|
String? coverUrl,
|
||||||
sourceType: sourceType ?? _sourceType,
|
String? type,
|
||||||
coverUrl: coverUrl ?? _coverUrl,
|
}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl,
|
||||||
type: type ?? _type,
|
sourceType: sourceType ?? _sourceType,
|
||||||
);
|
coverUrl: coverUrl ?? _coverUrl,
|
||||||
String? get sourceUrl => _sourceUrl;
|
type: type ?? _type,
|
||||||
num? get sourceType => _sourceType;
|
);
|
||||||
String? get coverUrl => _coverUrl;
|
String? get sourceUrl => _sourceUrl;
|
||||||
String? get type => _type;
|
num? get sourceType => _sourceType;
|
||||||
|
String? get coverUrl => _coverUrl;
|
||||||
Map<String, dynamic> toJson() {
|
String? get type => _type;
|
||||||
final map = <String, dynamic>{};
|
|
||||||
map['sourceUrl'] = _sourceUrl;
|
Map<String, dynamic> toJson() {
|
||||||
map['sourceType'] = _sourceType;
|
final map = <String, dynamic>{};
|
||||||
map['coverUrl'] = _coverUrl;
|
map['sourceUrl'] = _sourceUrl;
|
||||||
map['type'] = _type;
|
map['sourceType'] = _sourceType;
|
||||||
return map;
|
map['coverUrl'] = _coverUrl;
|
||||||
}
|
map['type'] = _type;
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
String? _stringOrNull(dynamic value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
num? _numOrNull(dynamic value) {
|
||||||
|
if (value is num) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return num.tryParse(value?.toString() ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool? _boolOrNull(dynamic value) {
|
||||||
|
if (value is bool) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value != 0;
|
||||||
|
}
|
||||||
|
final normalized = value?.toString().trim().toLowerCase();
|
||||||
|
if (normalized == "true" || normalized == "1") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalized == "false" || normalized == "0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,61 @@ import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
|||||||
|
|
||||||
typedef FloatingScreenManager = OverlayManager;
|
typedef FloatingScreenManager = OverlayManager;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool isFloatingAllowedRouteName(
|
||||||
|
String? routeName, {
|
||||||
|
required bool rootTabAllowed,
|
||||||
|
}) {
|
||||||
|
final normalizedRoute = (routeName ?? '').split('?').first.trim();
|
||||||
|
if (normalizedRoute.isEmpty || normalizedRoute == '/') {
|
||||||
|
return rootTabAllowed;
|
||||||
|
}
|
||||||
|
return normalizedRoute == '/main/messagePage' ||
|
||||||
|
normalizedRoute == '/chat' ||
|
||||||
|
normalizedRoute == '/systemChat' ||
|
||||||
|
normalizedRoute == '/room';
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool shouldDismissFloatingForLeftSwipe(
|
||||||
|
double dragOffset, {
|
||||||
|
double threshold = 72,
|
||||||
|
}) {
|
||||||
|
return dragOffset <= -threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SCFloatingScreenRouteObserver extends NavigatorObserver {
|
||||||
|
void _update(Route<dynamic>? route) {
|
||||||
|
if (route is PageRoute<dynamic>) {
|
||||||
|
OverlayManager().setCurrentPageRouteName(route.settings.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
super.didPush(route, previousRoute);
|
||||||
|
_update(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
super.didPop(route, previousRoute);
|
||||||
|
_update(previousRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
|
||||||
|
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
|
||||||
|
_update(newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||||
|
super.didRemove(route, previousRoute);
|
||||||
|
_update(previousRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OverlayManager {
|
class OverlayManager {
|
||||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||||
@ -48,6 +103,8 @@ class OverlayManager {
|
|||||||
OverlayEntry? _currentOverlayEntry;
|
OverlayEntry? _currentOverlayEntry;
|
||||||
SCFloatingMessage? _currentMessage;
|
SCFloatingMessage? _currentMessage;
|
||||||
bool _homeRootTabsVisible = false;
|
bool _homeRootTabsVisible = false;
|
||||||
|
String? _currentPageRouteName;
|
||||||
|
bool _allowedPageRefreshScheduled = false;
|
||||||
int _suppressedCount = 0;
|
int _suppressedCount = 0;
|
||||||
|
|
||||||
bool _isProcessing = false;
|
bool _isProcessing = false;
|
||||||
@ -184,6 +241,10 @@ class OverlayManager {
|
|||||||
return 'global_lucky_gift_win_fallback|$roomId|$userId|$toUserId|$giftId|$coins|$multiple';
|
return 'global_lucky_gift_win_fallback|$roomId|$userId|$toUserId|$giftId|$coins|$multiple';
|
||||||
}
|
}
|
||||||
if (message.type == 6) {
|
if (message.type == 6) {
|
||||||
|
final eventId = message.gameId?.trim() ?? '';
|
||||||
|
if (eventId.isNotEmpty) {
|
||||||
|
return 'cp_relation_event|$eventId';
|
||||||
|
}
|
||||||
final roomId = message.roomId?.trim() ?? '';
|
final roomId = message.roomId?.trim() ?? '';
|
||||||
final action = message.giftId?.trim() ?? '';
|
final action = message.giftId?.trim() ?? '';
|
||||||
final relationType = message.giftTab?.trim() ?? '';
|
final relationType = message.giftTab?.trim() ?? '';
|
||||||
@ -435,58 +496,68 @@ class OverlayManager {
|
|||||||
SCFloatingMessage message,
|
SCFloatingMessage message,
|
||||||
VoidCallback onComplete,
|
VoidCallback onComplete,
|
||||||
) {
|
) {
|
||||||
|
final Widget child;
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 0:
|
case 0:
|
||||||
return FloatingLuckGiftScreenWidget(
|
child = FloatingLuckGiftScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
//房间礼物
|
//房间礼物
|
||||||
return FloatingGiftScreenWidget(
|
child = FloatingGiftScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
//游戏中奖
|
//游戏中奖
|
||||||
return FloatingGameScreenWidget(
|
child = FloatingGameScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case FloatingLuckyGiftGlobalScreenWidget.floatingType:
|
case FloatingLuckyGiftGlobalScreenWidget.floatingType:
|
||||||
// Global lucky gift win.
|
// Global lucky gift win.
|
||||||
return FloatingLuckyGiftGlobalScreenWidget(
|
child = FloatingLuckyGiftGlobalScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
//火箭
|
//火箭
|
||||||
return FloatingRoomRocketScreenWidget(
|
child = FloatingRoomRocketScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
//红包
|
//红包
|
||||||
return FloatingRoomRedenvelopeScreenWidget(
|
child = FloatingRoomRedenvelopeScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
//VIP进房飘窗
|
//VIP进房飘窗
|
||||||
return FloatingVipEntryScreenWidget(
|
child = FloatingVipEntryScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
//CP / 挚友关系飘窗
|
//CP / 挚友关系飘窗
|
||||||
return FloatingCpRelationScreenWidget(
|
child = FloatingCpRelationScreenWidget(
|
||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
onComplete();
|
onComplete();
|
||||||
return Container();
|
child = const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
return _SwipeToDismissFloating(onDismiss: onComplete, child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _stackSlotKeyFor(SCFloatingMessage message) {
|
int? _stackSlotKeyFor(SCFloatingMessage message) {
|
||||||
@ -616,9 +687,39 @@ class OverlayManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_homeRootTabsVisible = visible;
|
_homeRootTabsVisible = visible;
|
||||||
if (!visible) {
|
_scheduleAllowedPageVisibilityRefresh();
|
||||||
_removeActiveRegionBroadcastMessage();
|
}
|
||||||
_removeRegionBroadcastMessages();
|
|
||||||
|
void setCurrentPageRouteName(String? routeName) {
|
||||||
|
if (_currentPageRouteName == routeName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_currentPageRouteName = routeName;
|
||||||
|
_scheduleAllowedPageVisibilityRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleAllowedPageVisibilityRefresh() {
|
||||||
|
if (_allowedPageRefreshScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_allowedPageRefreshScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_allowedPageRefreshScheduled = false;
|
||||||
|
if (!_isDisposed) {
|
||||||
|
_refreshAllowedPageVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isAllowedPageVisible => isFloatingAllowedRouteName(
|
||||||
|
_currentPageRouteName,
|
||||||
|
rootTabAllowed: _homeRootTabsVisible,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _refreshAllowedPageVisibility() {
|
||||||
|
if (!_isAllowedPageVisible) {
|
||||||
|
_removeActiveMessage(scheduleNext: false);
|
||||||
|
_removeAllStackedMessages();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_restoreDeferredRegionRedPacketMessages();
|
_restoreDeferredRegionRedPacketMessages();
|
||||||
@ -672,6 +773,8 @@ class OverlayManager {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
_homeRootTabsVisible = false;
|
_homeRootTabsVisible = false;
|
||||||
|
_currentPageRouteName = null;
|
||||||
|
_allowedPageRefreshScheduled = false;
|
||||||
_suppressedCount = 0;
|
_suppressedCount = 0;
|
||||||
_currentOverlayEntry?.remove();
|
_currentOverlayEntry?.remove();
|
||||||
_currentOverlayEntry = null;
|
_currentOverlayEntry = null;
|
||||||
@ -846,6 +949,9 @@ class OverlayManager {
|
|||||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!_isAllowedPageVisible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (message.isRegionBroadcast) {
|
if (message.isRegionBroadcast) {
|
||||||
return _shouldDisplayRegionBroadcastMessage(context, message);
|
return _shouldDisplayRegionBroadcastMessage(context, message);
|
||||||
}
|
}
|
||||||
@ -897,6 +1003,9 @@ class OverlayManager {
|
|||||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!_isAllowedPageVisible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
@ -937,13 +1046,13 @@ class OverlayManager {
|
|||||||
if (rtcProvider.shouldShowRoomVisualEffects) {
|
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
return SCGlobalConfig.isFloatingBroadcastAll && _isAllowedPageVisible;
|
||||||
}
|
}
|
||||||
if (isGlobalStackedBroadcast && rtcProvider.shouldShowRoomVisualEffects) {
|
if (isGlobalStackedBroadcast && rtcProvider.shouldShowRoomVisualEffects) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (isGlobalStackedBroadcast) {
|
if (isGlobalStackedBroadcast) {
|
||||||
return _homeRootTabsVisible;
|
return _isAllowedPageVisible;
|
||||||
}
|
}
|
||||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||||
return false;
|
return false;
|
||||||
@ -951,37 +1060,7 @@ class OverlayManager {
|
|||||||
if (rtcProvider.shouldShowRoomVisualEffects) {
|
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
return SCGlobalConfig.isFloatingBroadcastAll && _isAllowedPageVisible;
|
||||||
}
|
|
||||||
|
|
||||||
void _removeRegionBroadcastMessages() {
|
|
||||||
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
|
||||||
(a, b) => b.priority.compareTo(a.priority),
|
|
||||||
);
|
|
||||||
|
|
||||||
while (_messageQueue.isNotEmpty) {
|
|
||||||
final message = _messageQueue.removeFirst();
|
|
||||||
if (!message.isRegionBroadcast ||
|
|
||||||
_isCrossRoomRegionFloatingMessage(message)) {
|
|
||||||
newQueue.add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (newQueue.isNotEmpty) {
|
|
||||||
_messageQueue.add(newQueue.removeFirst());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeActiveRegionBroadcastMessage() {
|
|
||||||
final activeMessage = _currentMessage;
|
|
||||||
if (activeMessage != null && activeMessage.isRegionBroadcast) {
|
|
||||||
_currentOverlayEntry?.remove();
|
|
||||||
_currentOverlayEntry = null;
|
|
||||||
_currentMessage = null;
|
|
||||||
_isPlaying = false;
|
|
||||||
}
|
|
||||||
_removeStackedMessagesWhere((message) => message.isRegionBroadcast);
|
|
||||||
_safeScheduleNext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeActiveRoomMessage({
|
void _removeActiveRoomMessage({
|
||||||
@ -1065,3 +1144,57 @@ class OverlayManager {
|
|||||||
_markStackedEntriesNeedsBuild();
|
_markStackedEntriesNeedsBuild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SwipeToDismissFloating extends StatefulWidget {
|
||||||
|
const _SwipeToDismissFloating({required this.child, required this.onDismiss});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SwipeToDismissFloating> createState() =>
|
||||||
|
_SwipeToDismissFloatingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeToDismissFloatingState extends State<_SwipeToDismissFloating> {
|
||||||
|
double _dragOffset = 0;
|
||||||
|
|
||||||
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
|
final nextOffset = _dragOffset + details.delta.dx;
|
||||||
|
if (nextOffset > 0) {
|
||||||
|
if (_dragOffset != 0) {
|
||||||
|
setState(() => _dragOffset = 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _dragOffset = nextOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
|
if (shouldDismissFloatingForLeftSwipe(_dragOffset)) {
|
||||||
|
widget.onDismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_dragOffset != 0) {
|
||||||
|
setState(() => _dragOffset = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onHorizontalDragUpdate: _handleDragUpdate,
|
||||||
|
onHorizontalDragEnd: _handleDragEnd,
|
||||||
|
onHorizontalDragCancel: () {
|
||||||
|
if (_dragOffset != 0) {
|
||||||
|
setState(() => _dragOffset = 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(_dragOffset, 0),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
83
lib/shared/tools/sc_chat_bubble_message_data.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
|
||||||
|
const String _chatBubbleSnapshotKey = 'yumiChatBubbleV1';
|
||||||
|
|
||||||
|
class SCChatBubbleMessageSnapshot {
|
||||||
|
final bool hasSnapshot;
|
||||||
|
final String imageUrl;
|
||||||
|
|
||||||
|
const SCChatBubbleMessageSnapshot({
|
||||||
|
required this.hasSnapshot,
|
||||||
|
required this.imageUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String scResolveChatBubbleImageUrl(PropsResources? resource) {
|
||||||
|
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(_isSvgaResource), ...candidates]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String scEncodeChatBubbleMessageSnapshot(PropsResources? resource) {
|
||||||
|
return jsonEncode(<String, dynamic>{
|
||||||
|
_chatBubbleSnapshotKey: scResolveChatBubbleImageUrl(resource),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SCChatBubbleMessageSnapshot scDecodeChatBubbleMessageSnapshot(
|
||||||
|
String? cloudCustomData,
|
||||||
|
) {
|
||||||
|
final data = cloudCustomData?.trim() ?? '';
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return const SCChatBubbleMessageSnapshot(hasSnapshot: false, imageUrl: '');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(data);
|
||||||
|
if (decoded is! Map || !decoded.containsKey(_chatBubbleSnapshotKey)) {
|
||||||
|
return const SCChatBubbleMessageSnapshot(
|
||||||
|
hasSnapshot: false,
|
||||||
|
imageUrl: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final imageUrl = decoded[_chatBubbleSnapshotKey];
|
||||||
|
if (imageUrl is! String) {
|
||||||
|
return const SCChatBubbleMessageSnapshot(
|
||||||
|
hasSnapshot: false,
|
||||||
|
imageUrl: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SCChatBubbleMessageSnapshot(
|
||||||
|
hasSnapshot: true,
|
||||||
|
imageUrl: imageUrl.trim(),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return const SCChatBubbleMessageSnapshot(hasSnapshot: false, imageUrl: '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSvgaResource(String? resource) {
|
||||||
|
final lower = resource?.trim().toLowerCase() ?? '';
|
||||||
|
return lower.endsWith('.svga') || lower.contains('.svga?');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
18
lib/shared/tools/sc_conversation_time_formatter.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
class SCConversationTimeFormatter {
|
||||||
|
const SCConversationTimeFormatter._();
|
||||||
|
|
||||||
|
static String format(DateTime date, {DateTime? now}) {
|
||||||
|
final current = now ?? DateTime.now();
|
||||||
|
if (date.year != current.year) {
|
||||||
|
return '${date.year}-${_twoDigits(date.month)}-${_twoDigits(date.day)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
final time = '${_twoDigits(date.hour)}.${_twoDigits(date.minute)}';
|
||||||
|
if (date.month != current.month || date.day != current.day) {
|
||||||
|
return '${_twoDigits(date.month)}-${_twoDigits(date.day)} $time';
|
||||||
|
}
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _twoDigits(int value) => value.toString().padLeft(2, '0');
|
||||||
|
}
|
||||||
@ -24,6 +24,9 @@ class SCGoogleAuthService {
|
|||||||
static Future<User?> signInWithGoogle() async {
|
static Future<User?> signInWithGoogle() async {
|
||||||
try {
|
try {
|
||||||
await initialize();
|
await initialize();
|
||||||
|
// 主动登录前清理上次账号,确保重新显示账号选择器
|
||||||
|
await _g.signOut();
|
||||||
|
await FirebaseAuth.instance.signOut();
|
||||||
// 触发谷歌登录
|
// 触发谷歌登录
|
||||||
final GoogleSignInAccount? googleAccount = await _g.signIn();
|
final GoogleSignInAccount? googleAccount = await _g.signIn();
|
||||||
if (googleAccount == null) return null;
|
if (googleAccount == null) return null;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -13,13 +12,12 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart';
|
|||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_sysytem_message_type.dart';
|
import 'package:yumi/shared/data_sources/models/enum/sc_sysytem_message_type.dart';
|
||||||
import 'package:yumi/shared/tools/sc_date_utils.dart';
|
import 'package:yumi/shared/tools/sc_conversation_time_formatter.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.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/login_res.dart';
|
||||||
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
||||||
@ -46,6 +44,7 @@ class _MessageConversationListPageState
|
|||||||
extends State<MessageConversationListPage> {
|
extends State<MessageConversationListPage> {
|
||||||
final Map<String, SocialChatUserProfile> _conversationUserCache = {};
|
final Map<String, SocialChatUserProfile> _conversationUserCache = {};
|
||||||
final Set<String> _loadingConversationUserIds = {};
|
final Set<String> _loadingConversationUserIds = {};
|
||||||
|
String? _deleteActionConversationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -69,177 +68,199 @@ class _MessageConversationListPageState
|
|||||||
if (searchQuery.isNotEmpty) {
|
if (searchQuery.isNotEmpty) {
|
||||||
_loadSearchUserInfo(provider.conversationList);
|
_loadSearchUserInfo(provider.conversationList);
|
||||||
}
|
}
|
||||||
return Column(
|
return GestureDetector(
|
||||||
children: [
|
behavior: HitTestBehavior.translucent,
|
||||||
Expanded(
|
onTap: _dismissDeleteAction,
|
||||||
child: CustomScrollView(
|
child: Column(
|
||||||
shrinkWrap: true,
|
children: [
|
||||||
slivers: <Widget>[
|
Expanded(
|
||||||
SliverToBoxAdapter(
|
child: CustomScrollView(
|
||||||
child:
|
shrinkWrap: true,
|
||||||
searchQuery.isEmpty &&
|
slivers: <Widget>[
|
||||||
provider.customerInfo != null &&
|
SliverToBoxAdapter(
|
||||||
provider.customerInfo?.id !=
|
child:
|
||||||
AccountStorage()
|
searchQuery.isEmpty &&
|
||||||
.getCurrentUser()
|
provider.customerInfo != null &&
|
||||||
?.userProfile
|
provider.customerInfo?.id !=
|
||||||
?.id
|
AccountStorage()
|
||||||
? Stack(
|
.getCurrentUser()
|
||||||
children: [
|
?.userProfile
|
||||||
Container(
|
?.id
|
||||||
margin: EdgeInsets.symmetric(
|
? Stack(
|
||||||
horizontal: 15.w,
|
children: [
|
||||||
),
|
Container(
|
||||||
child: Row(
|
margin: EdgeInsets.symmetric(
|
||||||
children: [
|
horizontal: 15.w,
|
||||||
Image.asset(
|
),
|
||||||
"sc_images/general/sc_icon_logo.png",
|
child: Row(
|
||||||
height: 48.w,
|
children: [
|
||||||
),
|
Image.asset(
|
||||||
SizedBox(width: 10.w),
|
"sc_images/general/sc_icon_logo.png",
|
||||||
Expanded(
|
height: 48.w,
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment:
|
SizedBox(width: 10.w),
|
||||||
CrossAxisAlignment.start,
|
Expanded(
|
||||||
children: [
|
child: Column(
|
||||||
text(
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
text(
|
||||||
|
SCAppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.contactUs,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
textColor:
|
||||||
|
widget.isRoom
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
fontSize: 15.sp,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"[${SCAppLocalizations.of(context)!.newMessage}]",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil()
|
||||||
|
.setSp(13),
|
||||||
|
color:
|
||||||
|
widget.isRoom
|
||||||
|
? Colors.white54
|
||||||
|
: Color(
|
||||||
|
0xFF666666,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 5.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SCDebounceWidget(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
width: 88.w,
|
||||||
|
height: 26.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(18.w),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xff963EFB),
|
||||||
|
Color(0xffDCAEF6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: text(
|
||||||
SCAppLocalizations.of(
|
SCAppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
)!.contactUs,
|
)!.contactUs,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
textColor:
|
textColor: Colors.white,
|
||||||
widget.isRoom
|
fontSize: 13.sp,
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
fontSize: 15.sp,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
"[${SCAppLocalizations.of(context)!.newMessage}]",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow:
|
|
||||||
TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: ScreenUtil()
|
|
||||||
.setSp(13),
|
|
||||||
color:
|
|
||||||
widget.isRoom
|
|
||||||
? Colors.white54
|
|
||||||
: Color(
|
|
||||||
0xFF666666,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SCDebounceWidget(
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
width: 88.w,
|
|
||||||
height: 26.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
18.w,
|
|
||||||
),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xff963EFB),
|
|
||||||
Color(0xffDCAEF6),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: text(
|
onTap: () async {
|
||||||
SCAppLocalizations.of(
|
if (_dismissDeleteAction()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var conversation =
|
||||||
|
V2TimConversation(
|
||||||
|
type:
|
||||||
|
ConversationType
|
||||||
|
.V2TIM_C2C,
|
||||||
|
userID:
|
||||||
|
provider.customerInfo?.id,
|
||||||
|
conversationID: '',
|
||||||
|
);
|
||||||
|
provider.updateCustomerCount(0);
|
||||||
|
var bool =
|
||||||
|
await Provider.of<RtmProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).startConversation(
|
||||||
|
conversation,
|
||||||
|
);
|
||||||
|
if (!bool) return;
|
||||||
|
var json = jsonEncode(
|
||||||
|
conversation.toJson(),
|
||||||
|
);
|
||||||
|
SCNavigatorUtils.push(
|
||||||
context,
|
context,
|
||||||
)!.contactUs,
|
"${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}",
|
||||||
fontWeight: FontWeight.w600,
|
);
|
||||||
textColor: Colors.white,
|
},
|
||||||
fontSize: 13.sp,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onTap: () async {
|
],
|
||||||
var conversation = V2TimConversation(
|
),
|
||||||
type: ConversationType.V2TIM_C2C,
|
|
||||||
userID: provider.customerInfo?.id,
|
|
||||||
conversationID: '',
|
|
||||||
);
|
|
||||||
provider.updateCustomerCount(0);
|
|
||||||
var bool =
|
|
||||||
await Provider.of<RtmProvider>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
).startConversation(conversation);
|
|
||||||
if (!bool) return;
|
|
||||||
var json = jsonEncode(
|
|
||||||
conversation.toJson(),
|
|
||||||
);
|
|
||||||
SCNavigatorUtils.push(
|
|
||||||
context,
|
|
||||||
"${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
// 未读数量
|
||||||
// 未读数量
|
PositionedDirectional(
|
||||||
PositionedDirectional(
|
bottom: 0,
|
||||||
bottom: 0,
|
end: 10.w,
|
||||||
end: 10.w,
|
child: Visibility(
|
||||||
child: Visibility(
|
visible: provider.customerUnReadCount > 0,
|
||||||
visible: provider.customerUnReadCount > 0,
|
child: Container(
|
||||||
child: Container(
|
padding: EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.symmetric(
|
horizontal: 6.w,
|
||||||
horizontal: 6.w,
|
),
|
||||||
),
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
height: 16.w,
|
||||||
height: 16.w,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
//gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]),
|
||||||
//gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]),
|
color: Colors.red,
|
||||||
color: Colors.red,
|
shape: BoxShape.circle,
|
||||||
shape: BoxShape.circle,
|
//borderRadius: BorderRadius.all(Radius.circular(7.w)),
|
||||||
//borderRadius: BorderRadius.all(Radius.circular(7.w)),
|
),
|
||||||
),
|
child: Text(
|
||||||
child: Text(
|
"${provider.customerUnReadCount > 99 ? '99+' : provider.customerUnReadCount}",
|
||||||
"${provider.customerUnReadCount > 99 ? '99+' : provider.customerUnReadCount}",
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 10.sp,
|
||||||
fontSize: 10.sp,
|
color: Color(0xffffffff),
|
||||||
color: Color(0xffffffff),
|
fontWeight: FontWeight.w400,
|
||||||
fontWeight: FontWeight.w400,
|
decoration: TextDecoration.none,
|
||||||
decoration: TextDecoration.none,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
)
|
||||||
)
|
: Container(),
|
||||||
: Container(),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _msgListView(
|
|
||||||
conversationList,
|
|
||||||
isRoom: widget.isRoom,
|
|
||||||
),
|
),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
],
|
child: _msgListView(
|
||||||
|
conversationList,
|
||||||
|
isRoom: widget.isRoom,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _dismissDeleteAction() {
|
||||||
|
if (_deleteActionConversationId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_deleteActionConversationId = null;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _msgListView(
|
Widget _msgListView(
|
||||||
List<V2TimConversation> chatList, {
|
List<V2TimConversation> chatList, {
|
||||||
EdgeInsetsGeometry? margin,
|
EdgeInsetsGeometry? margin,
|
||||||
@ -269,9 +290,9 @@ class _MessageConversationListPageState
|
|||||||
),
|
),
|
||||||
separatorBuilder:
|
separatorBuilder:
|
||||||
(_, index) => Divider(
|
(_, index) => Divider(
|
||||||
color: Colors.black12,
|
color: const Color(0xFF0E5B42).withValues(alpha: 0.7),
|
||||||
indent: 65.w,
|
indent: 64.w,
|
||||||
endIndent: 15.w,
|
endIndent: 0,
|
||||||
height: 0.5.w,
|
height: 0.5.w,
|
||||||
thickness: 0.5.w,
|
thickness: 0.5.w,
|
||||||
),
|
),
|
||||||
@ -291,6 +312,28 @@ class _MessageConversationListPageState
|
|||||||
index: index,
|
index: index,
|
||||||
isRoom: isRoom,
|
isRoom: isRoom,
|
||||||
initialUser: _cachedConversationUser(conversation),
|
initialUser: _cachedConversationUser(conversation),
|
||||||
|
isDeleteActionVisible:
|
||||||
|
_deleteActionConversationId == conversation.conversationID,
|
||||||
|
isAnyDeleteActionVisible: _deleteActionConversationId != null,
|
||||||
|
onDismissDeleteAction: _dismissDeleteAction,
|
||||||
|
onLongPress: () {
|
||||||
|
setState(() {
|
||||||
|
_deleteActionConversationId = conversation.conversationID;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDelete: () async {
|
||||||
|
await Provider.of<RtmProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).deleteConversationById(conversation.conversationID);
|
||||||
|
if (!mounted ||
|
||||||
|
_deleteActionConversationId != conversation.conversationID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_deleteActionConversationId = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
key: Key(conversation.conversationID),
|
key: Key(conversation.conversationID),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -384,6 +427,11 @@ class ConversationItem extends StatefulWidget {
|
|||||||
final int index;
|
final int index;
|
||||||
final bool isRoom;
|
final bool isRoom;
|
||||||
final SocialChatUserProfile? initialUser;
|
final SocialChatUserProfile? initialUser;
|
||||||
|
final bool isDeleteActionVisible;
|
||||||
|
final bool isAnyDeleteActionVisible;
|
||||||
|
final VoidCallback onDismissDeleteAction;
|
||||||
|
final VoidCallback onLongPress;
|
||||||
|
final Future<void> Function() onDelete;
|
||||||
|
|
||||||
const ConversationItem({
|
const ConversationItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -391,6 +439,11 @@ class ConversationItem extends StatefulWidget {
|
|||||||
required this.index,
|
required this.index,
|
||||||
required this.isRoom,
|
required this.isRoom,
|
||||||
this.initialUser,
|
this.initialUser,
|
||||||
|
required this.isDeleteActionVisible,
|
||||||
|
required this.isAnyDeleteActionVisible,
|
||||||
|
required this.onDismissDeleteAction,
|
||||||
|
required this.onLongPress,
|
||||||
|
required this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -526,6 +579,10 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
}*/
|
}*/
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
if (widget.isAnyDeleteActionVisible) {
|
||||||
|
widget.onDismissDeleteAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_isC2CConversation && _effectiveUserId.isEmpty) {
|
if (_isC2CConversation && _effectiveUserId.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -551,9 +608,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
"$route?conversation=${Uri.encodeComponent(json)}",
|
"$route?conversation=${Uri.encodeComponent(json)}",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: widget.onLongPress,
|
||||||
showDeleteConfirm();
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
//color: Colors.white,
|
//color: Colors.white,
|
||||||
@ -562,15 +617,19 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: ScreenUtil().screenWidth,
|
width: ScreenUtil().screenWidth,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
height: 76.w,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 10.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
//color: Colors.white,
|
color:
|
||||||
|
widget.isDeleteActionVisible
|
||||||
|
? Colors.white.withValues(alpha: 0.06)
|
||||||
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
head(url: _displayAvatar, width: 45.w),
|
head(url: _displayAvatar, width: 56.w),
|
||||||
SizedBox(width: 6.w),
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -585,11 +644,8 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
maxWidth: 152.w,
|
maxWidth: 152.w,
|
||||||
_displayName,
|
_displayName,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w700,
|
||||||
textColor:
|
textColor: Colors.white,
|
||||||
widget.isRoom
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
type:
|
type:
|
||||||
_isC2CConversation
|
_isC2CConversation
|
||||||
? user?.getVIP()?.name ?? ""
|
? user?.getVIP()?.name ?? ""
|
||||||
@ -602,8 +658,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
),
|
),
|
||||||
SizedBox(width: 6.w),
|
SizedBox(width: 6.w),
|
||||||
Text(
|
Text(
|
||||||
SCMDateUtils.formatMessageTime(
|
SCConversationTimeFormatter.format(
|
||||||
context,
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
(conversation.lastMessage!.timestamp ?? 0) *
|
(conversation.lastMessage!.timestamp ?? 0) *
|
||||||
1000,
|
1000,
|
||||||
@ -611,10 +666,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13.sp,
|
fontSize: 13.sp,
|
||||||
color:
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
widget.isRoom
|
|
||||||
? Colors.white54
|
|
||||||
: Color(0xff8d8d8d),
|
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
height: 1,
|
height: 1,
|
||||||
@ -632,10 +684,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: ScreenUtil().setSp(13),
|
fontSize: ScreenUtil().setSp(13),
|
||||||
color:
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
widget.isRoom
|
|
||||||
? Colors.white54
|
|
||||||
: Color(0xFF666666),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -669,6 +718,21 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.isDeleteActionVisible)
|
||||||
|
SCDebounceWidget(
|
||||||
|
onTap: widget.onDelete,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
"sc_images/msg/sc_icon_msg_menu_delete.png",
|
||||||
|
width: 28.w,
|
||||||
|
height: 28.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -678,28 +742,6 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showDeleteConfirm() {
|
|
||||||
SmartDialog.show(
|
|
||||||
tag: "showConfirmDialog",
|
|
||||||
alignment: Alignment.center,
|
|
||||||
debounce: true,
|
|
||||||
animationType: SmartAnimationType.fade,
|
|
||||||
builder: (_) {
|
|
||||||
return MsgDialog(
|
|
||||||
title: SCAppLocalizations.of(context)!.tips,
|
|
||||||
msg: SCAppLocalizations.of(context)!.deleteConversationTips,
|
|
||||||
btnText: SCAppLocalizations.of(context)!.confirm,
|
|
||||||
onEnsure: () async {
|
|
||||||
Provider.of<RtmProvider>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
).clearC2CHistoryMessage(conversation.conversationID, true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadUserInfo() {
|
void loadUserInfo() {
|
||||||
if (!_shouldLoadUserInfo) return;
|
if (!_shouldLoadUserInfo) return;
|
||||||
final userId = _effectiveUserId;
|
final userId = _effectiveUserId;
|
||||||
|
|||||||
@ -368,11 +368,13 @@ class _RoomCpInviteDialogState extends State<RoomCpInviteDialog> {
|
|||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
if (widget.actionState != RoomCpInviteActionState.accepted &&
|
||||||
left: 65.w,
|
widget.actionState != RoomCpInviteActionState.rejected)
|
||||||
top: 74.w,
|
Positioned(
|
||||||
child: _CountdownRow(text: _countdownText),
|
left: 65.w,
|
||||||
),
|
top: 74.w,
|
||||||
|
child: _CountdownRow(text: _countdownText),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -32,9 +32,11 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<RtcProvider, SCRoomRocketStatusRes?>(
|
final provider = context.read<RtcProvider>();
|
||||||
selector: (_, provider) => provider.roomRocketStatus,
|
return AnimatedBuilder(
|
||||||
builder: (context, status, child) {
|
animation: provider,
|
||||||
|
builder: (context, child) {
|
||||||
|
final status = provider.roomRocketStatus;
|
||||||
if (status == null || !status.isEnabled) {
|
if (status == null || !status.isEnabled) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,9 +38,11 @@ class RoomGiftComboFloatingLayer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<RtcProvider, bool>(
|
final provider = context.read<RtcProvider>();
|
||||||
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
|
return AnimatedBuilder(
|
||||||
builder: (context, showMic, child) {
|
animation: provider,
|
||||||
|
builder: (context, child) {
|
||||||
|
final showMic = _shouldShowRoomBottomMic(provider);
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final inputWidth = _resolveRoomBottomInputWidth(
|
final inputWidth = _resolveRoomBottomInputWidth(
|
||||||
@ -81,15 +83,16 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.read<RtcProvider>();
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _floatingButtonHostHeight.w,
|
height: _floatingButtonHostHeight.w,
|
||||||
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
child: AnimatedBuilder(
|
||||||
selector:
|
animation: provider,
|
||||||
(context, provider) => _RoomBottomSnapshot(
|
builder: (context, child) {
|
||||||
showMic: _shouldShowRoomBottomMic(provider),
|
final bottomSnapshot = _RoomBottomSnapshot(
|
||||||
isMic: provider.isMic,
|
showMic: _shouldShowRoomBottomMic(provider),
|
||||||
),
|
isMic: provider.isMic,
|
||||||
builder: (context, bottomSnapshot, child) {
|
);
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final inputWidth = _resolveRoomBottomInputWidth(
|
final inputWidth = _resolveRoomBottomInputWidth(
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||||
|
|
||||||
class RoomEmojiAssetImage extends StatefulWidget {
|
class RoomEmojiAssetImage extends StatefulWidget {
|
||||||
const RoomEmojiAssetImage({
|
const RoomEmojiAssetImage({
|
||||||
@ -20,12 +22,12 @@ class RoomEmojiAssetImage extends StatefulWidget {
|
|||||||
|
|
||||||
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
late AssetImage _imageProvider;
|
AssetImage? _imageProvider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_imageProvider = AssetImage(widget.asset);
|
_syncImageProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -38,7 +40,7 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.asset != widget.asset) {
|
if (oldWidget.asset != widget.asset) {
|
||||||
_imageProvider = AssetImage(widget.asset);
|
_syncImageProvider();
|
||||||
_precache();
|
_precache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,9 +48,42 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
final resource = widget.asset.trim();
|
||||||
|
if (resource.isEmpty) {
|
||||||
|
return SizedBox(width: widget.width, height: widget.height);
|
||||||
|
}
|
||||||
|
if (SCNetworkSvgaWidget.isSvga(resource)) {
|
||||||
|
return RepaintBoundary(
|
||||||
|
child: SCNetworkSvgaWidget(
|
||||||
|
resource: resource,
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
loop: true,
|
||||||
|
fit: widget.fit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!_isLocalAsset(resource)) {
|
||||||
|
return RepaintBoundary(
|
||||||
|
child: netImage(
|
||||||
|
url: resource,
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
fit: widget.fit,
|
||||||
|
gifFit: widget.fit,
|
||||||
|
noDefaultImg: true,
|
||||||
|
loadingWidget: SizedBox(width: widget.width, height: widget.height),
|
||||||
|
errorWidget: SizedBox(width: widget.width, height: widget.height),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final imageProvider = _imageProvider;
|
||||||
|
if (imageProvider == null) {
|
||||||
|
return SizedBox(width: widget.width, height: widget.height);
|
||||||
|
}
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: Image(
|
child: Image(
|
||||||
image: _imageProvider,
|
image: imageProvider,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
@ -59,7 +94,19 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _precache() {
|
void _precache() {
|
||||||
precacheImage(_imageProvider, context);
|
final imageProvider = _imageProvider;
|
||||||
|
if (imageProvider != null) {
|
||||||
|
precacheImage(imageProvider, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncImageProvider() {
|
||||||
|
final resource = widget.asset.trim();
|
||||||
|
_imageProvider = _isLocalAsset(resource) ? AssetImage(resource) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isLocalAsset(String resource) {
|
||||||
|
return resource.startsWith("assets/") || resource.startsWith("sc_images/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import 'package:yumi/app/constants/sc_screen.dart';
|
|||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||||
@ -34,34 +36,34 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
|
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
|
||||||
_RoomEmojiCategory(
|
_RoomEmojiCategory(
|
||||||
id: "fluent",
|
id: "fluent",
|
||||||
iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
|
iconResource: "sc_images/room/emoji/fluent_emoji_01.webp",
|
||||||
assets: [
|
items: [
|
||||||
"sc_images/room/emoji/fluent_emoji_01.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_01.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_02.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_02.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_03.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_03.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_04.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_04.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_05.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_05.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_06.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_06.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_07.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_07.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_08.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_08.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_09.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_09.webp"),
|
||||||
"sc_images/room/emoji/fluent_emoji_10.webp",
|
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_10.webp"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_RoomEmojiCategory(
|
_RoomEmojiCategory(
|
||||||
id: "monkey",
|
id: "monkey",
|
||||||
iconAsset: "sc_images/room/emoji/monkey_1.gif",
|
iconResource: "sc_images/room/emoji/monkey_1.gif",
|
||||||
assets: [
|
items: [
|
||||||
"sc_images/room/emoji/monkey_1.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_1.gif"),
|
||||||
"sc_images/room/emoji/monkey_2.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_2.gif"),
|
||||||
"sc_images/room/emoji/monkey_3.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_3.gif"),
|
||||||
"sc_images/room/emoji/monkey_4.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_4.gif"),
|
||||||
"sc_images/room/emoji/monkey_5.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_5.gif"),
|
||||||
"sc_images/room/emoji/monkey_6.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_6.gif"),
|
||||||
"sc_images/room/emoji/monkey_7.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_7.gif"),
|
||||||
"sc_images/room/emoji/monkey_8.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_8.gif"),
|
||||||
"sc_images/room/emoji/monkey_9.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_9.gif"),
|
||||||
"sc_images/room/emoji/monkey_10.gif",
|
_RoomEmojiItem.local("sc_images/room/emoji/monkey_10.gif"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -99,6 +101,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
if (showEmoji) {
|
if (showEmoji) {
|
||||||
_precacheRoomEmojis();
|
_precacheRoomEmojis();
|
||||||
|
_loadRemoteRoomEmojis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +250,12 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmojiPanel() {
|
Widget _buildEmojiPanel() {
|
||||||
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
|
final categories = _resolveRoomEmojiCategories();
|
||||||
|
final selectedIndex = _selectedEmojiCategoryIndex.clamp(
|
||||||
|
0,
|
||||||
|
categories.length - 1,
|
||||||
|
);
|
||||||
|
final selectedCategory = categories[selectedIndex];
|
||||||
return Container(
|
return Container(
|
||||||
height: 220.w,
|
height: 220.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -265,18 +273,18 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
crossAxisSpacing: 18.w,
|
crossAxisSpacing: 18.w,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
),
|
),
|
||||||
itemCount: selectedCategory.assets.length,
|
itemCount: selectedCategory.items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final emojiAsset = selectedCategory.assets[index];
|
final emojiItem = selectedCategory.items[index];
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_sendRoomEmoji(emojiAsset);
|
_sendRoomEmoji(emojiItem.sendResource);
|
||||||
},
|
},
|
||||||
child: Center(
|
child: Center(
|
||||||
child: RoomEmojiAssetImage(
|
child: RoomEmojiAssetImage(
|
||||||
key: ValueKey(emojiAsset),
|
key: ValueKey(emojiItem.displayResource),
|
||||||
asset: emojiAsset,
|
asset: emojiItem.displayResource,
|
||||||
width: 42.w,
|
width: 42.w,
|
||||||
height: 42.w,
|
height: 42.w,
|
||||||
),
|
),
|
||||||
@ -285,13 +293,16 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildEmojiCategoryBar(),
|
_buildEmojiCategoryBar(categories, selectedIndex),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmojiCategoryBar() {
|
Widget _buildEmojiCategoryBar(
|
||||||
|
List<_RoomEmojiCategory> categories,
|
||||||
|
int selectedIndex,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 50.w,
|
height: 50.w,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
|
||||||
@ -303,11 +314,11 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
),
|
),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: _roomEmojiCategories.length,
|
itemCount: categories.length,
|
||||||
separatorBuilder: (_, __) => SizedBox(width: 10.w),
|
separatorBuilder: (_, __) => SizedBox(width: 10.w),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final category = _roomEmojiCategories[index];
|
final category = categories[index];
|
||||||
final selected = index == _selectedEmojiCategoryIndex;
|
final selected = index == selectedIndex;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -340,7 +351,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: RoomEmojiAssetImage(
|
child: RoomEmojiAssetImage(
|
||||||
key: ValueKey("emoji_category_${category.id}"),
|
key: ValueKey("emoji_category_${category.id}"),
|
||||||
asset: category.iconAsset,
|
asset: category.iconResource,
|
||||||
width: 26.w,
|
width: 26.w,
|
||||||
height: 26.w,
|
height: 26.w,
|
||||||
),
|
),
|
||||||
@ -363,6 +374,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
|
|
||||||
SCKeybordUtil.hide(context);
|
SCKeybordUtil.hide(context);
|
||||||
_precacheRoomEmojis();
|
_precacheRoomEmojis();
|
||||||
|
_loadRemoteRoomEmojis();
|
||||||
setState(() {
|
setState(() {
|
||||||
showEmoji = true;
|
showEmoji = true;
|
||||||
});
|
});
|
||||||
@ -374,13 +386,28 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
}
|
}
|
||||||
_emojiAssetsPrecached = true;
|
_emojiAssetsPrecached = true;
|
||||||
for (final category in _roomEmojiCategories) {
|
for (final category in _roomEmojiCategories) {
|
||||||
precacheImage(AssetImage(category.iconAsset), context);
|
precacheImage(AssetImage(category.iconResource), context);
|
||||||
for (final asset in category.assets) {
|
for (final item in category.items) {
|
||||||
precacheImage(AssetImage(asset), context);
|
precacheImage(AssetImage(item.displayResource), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _loadRemoteRoomEmojis() {
|
||||||
|
Provider.of<SCAppGeneralManager>(context, listen: false).emojiAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_RoomEmojiCategory> _resolveRoomEmojiCategories() {
|
||||||
|
final remoteGroups =
|
||||||
|
Provider.of<SCAppGeneralManager>(context).roomEmojiGroups;
|
||||||
|
return [
|
||||||
|
..._roomEmojiCategories,
|
||||||
|
...remoteGroups
|
||||||
|
.map(_RoomEmojiCategory.fromRemote)
|
||||||
|
.where((category) => category.items.isNotEmpty),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void _sendRoomEmoji(String emojiAsset) {
|
void _sendRoomEmoji(String emojiAsset) {
|
||||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||||
final currenRoom = rtcProvider.currenRoom;
|
final currenRoom = rtcProvider.currenRoom;
|
||||||
@ -435,13 +462,54 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
class _RoomEmojiCategory {
|
class _RoomEmojiCategory {
|
||||||
const _RoomEmojiCategory({
|
const _RoomEmojiCategory({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.iconAsset,
|
required this.iconResource,
|
||||||
required this.assets,
|
required this.items,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory _RoomEmojiCategory.fromRemote(SCRoomEmojiRes group) {
|
||||||
|
final items =
|
||||||
|
(group.emojis ?? const <Emojis>[])
|
||||||
|
.map(_RoomEmojiItem.fromRemote)
|
||||||
|
.where((item) => item.sendResource.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final firstPreview =
|
||||||
|
items.isEmpty ? "" : items.first.displayResource.trim();
|
||||||
|
final cover = group.cover?.trim() ?? "";
|
||||||
|
final remoteId =
|
||||||
|
(group.id ?? group.groupCode ?? group.groupName ?? firstPreview).trim();
|
||||||
|
return _RoomEmojiCategory(
|
||||||
|
id: "remote_${remoteId.isEmpty ? firstPreview : remoteId}",
|
||||||
|
iconResource: cover.isNotEmpty ? cover : firstPreview,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String iconAsset;
|
final String iconResource;
|
||||||
final List<String> assets;
|
final List<_RoomEmojiItem> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoomEmojiItem {
|
||||||
|
const _RoomEmojiItem({
|
||||||
|
required this.displayResource,
|
||||||
|
required this.sendResource,
|
||||||
|
});
|
||||||
|
|
||||||
|
const _RoomEmojiItem.local(String resource)
|
||||||
|
: displayResource = resource,
|
||||||
|
sendResource = resource;
|
||||||
|
|
||||||
|
factory _RoomEmojiItem.fromRemote(Emojis emoji) {
|
||||||
|
final cover = emoji.coverUrl?.trim() ?? "";
|
||||||
|
final source = emoji.sourceUrl?.trim() ?? "";
|
||||||
|
return _RoomEmojiItem(
|
||||||
|
displayResource: cover.isNotEmpty ? cover : source,
|
||||||
|
sendResource: source.isNotEmpty ? source : cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String displayResource;
|
||||||
|
final String sendResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopRoute extends PopupRoute {
|
class PopRoute extends PopupRoute {
|
||||||
|
|||||||
@ -408,8 +408,8 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
|
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
|
||||||
final multilineMaxBubbleWidth = math.min(maxBubbleWidth, 300.w);
|
final multilineMaxBubbleWidth = math.min(maxBubbleWidth, 300.w);
|
||||||
final multilineMaxTextWidth = math.max(
|
final multilineMaxTextWidth = math.max(
|
||||||
82.w,
|
60.w,
|
||||||
multilineMaxBubbleWidth - 64.w,
|
multilineMaxBubbleWidth - 44.w,
|
||||||
);
|
);
|
||||||
final multilineTextPainter = TextPainter(
|
final multilineTextPainter = TextPainter(
|
||||||
text: TextSpan(text: messageText, style: textStyle),
|
text: TextSpan(text: messageText, style: textStyle),
|
||||||
@ -443,15 +443,15 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
required double maxBubbleWidth,
|
required double maxBubbleWidth,
|
||||||
required double startMargin,
|
required double startMargin,
|
||||||
}) {
|
}) {
|
||||||
final leftPadding = 22.w;
|
final leftPadding = 18.w;
|
||||||
final rightPadding = 42.w;
|
final rightPadding = 26.w;
|
||||||
final topPadding = 16.w;
|
final topPadding = 10.w;
|
||||||
final bottomPadding = 24.w;
|
final bottomPadding = 12.w;
|
||||||
final bubbleWidth = math
|
final bubbleWidth = math
|
||||||
.max(126.w, textSize.width + leftPadding + rightPadding)
|
.max(92.w, textSize.width + leftPadding + rightPadding)
|
||||||
.clamp(126.w, maxBubbleWidth);
|
.clamp(92.w, maxBubbleWidth);
|
||||||
final textMaxWidth = math.max(
|
final textMaxWidth = math.max(
|
||||||
64.w,
|
48.w,
|
||||||
bubbleWidth - leftPadding - rightPadding,
|
bubbleWidth - leftPadding - rightPadding,
|
||||||
);
|
);
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
@ -460,7 +460,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
)..layout(maxWidth: textMaxWidth);
|
)..layout(maxWidth: textMaxWidth);
|
||||||
final textBoxHeight = textPainter.size.height + 2.w;
|
final textBoxHeight = textPainter.size.height + 2.w;
|
||||||
final bubbleHeight = math.max(
|
final bubbleHeight = math.max(
|
||||||
76.w,
|
52.w,
|
||||||
textBoxHeight + topPadding + bottomPadding,
|
textBoxHeight + topPadding + bottomPadding,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -502,13 +502,13 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
required double maxBubbleWidth,
|
required double maxBubbleWidth,
|
||||||
required double startMargin,
|
required double startMargin,
|
||||||
}) {
|
}) {
|
||||||
final leftPadding = 22.w;
|
final leftPadding = 16.w;
|
||||||
final rightPadding = 24.w;
|
final rightPadding = 18.w;
|
||||||
final topPadding = 12.w;
|
final topPadding = 8.w;
|
||||||
final bottomPadding = 16.w;
|
final bottomPadding = 10.w;
|
||||||
final minSkinWidth = 96.w;
|
final minSkinWidth = 56.w;
|
||||||
final minSkinHeight = 58.w;
|
final minSkinHeight = 52.w;
|
||||||
final textWidthSafety = 10.w;
|
final textWidthSafety = 4.w;
|
||||||
final maxTextWidth = math.max(
|
final maxTextWidth = math.max(
|
||||||
1.0,
|
1.0,
|
||||||
maxBubbleWidth - leftPadding - rightPadding,
|
maxBubbleWidth - leftPadding - rightPadding,
|
||||||
@ -1183,78 +1183,107 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)),
|
padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)),
|
||||||
margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w),
|
margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w),
|
||||||
child: Column(
|
child: _buildGiftMessageItem(context),
|
||||||
children: [
|
);
|
||||||
_buildUserInfoLine(context),
|
}
|
||||||
SizedBox(height: 6.w),
|
|
||||||
Row(
|
Widget _buildGiftMessageItem(BuildContext context) {
|
||||||
|
final userId = widget.msg.user?.id;
|
||||||
|
_clearStaleUserInfoIfNeeded(userId);
|
||||||
|
if (userId == null || userId.isEmpty) {
|
||||||
|
return _buildGiftMessageItemWithUser(context, widget.msg.user);
|
||||||
|
}
|
||||||
|
final cachedUser = SCRoomUtils.roomUsersMap[userId];
|
||||||
|
if (cachedUser != null) {
|
||||||
|
return _buildGiftMessageItemWithUser(context, cachedUser);
|
||||||
|
}
|
||||||
|
return FutureBuilder<SocialChatUserProfile?>(
|
||||||
|
future: _loadRoomUserInfoSilently(userId),
|
||||||
|
builder: (ct, AsyncSnapshot<SocialChatUserProfile?> snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
final user = snapshot.data!;
|
||||||
|
SCRoomUtils.roomUsersMap[user.id ?? ""] = user;
|
||||||
|
return _buildGiftMessageItemWithUser(context, user);
|
||||||
|
}
|
||||||
|
return _buildGiftMessageItemWithUser(context, widget.msg.user);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGiftMessageItemWithUser(
|
||||||
|
BuildContext context,
|
||||||
|
SocialChatUserProfile? user,
|
||||||
|
) {
|
||||||
|
final badges = _activeWearBadges(user);
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildMessageUserAvatar(context, user),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
_buildUserNameAndLevels(user),
|
||||||
// 使用Flexible替代Expanded
|
_buildLongMedals(userId: user?.id, fallbackBadges: badges),
|
||||||
child: Container(
|
_buildMedals(userId: user?.id, fallbackBadges: badges),
|
||||||
constraints: BoxConstraints(
|
SizedBox(height: 2.w),
|
||||||
maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束
|
_buildGiftMessageBubble(context),
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black38,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topRight: Radius.circular(15.w),
|
|
||||||
bottomLeft: Radius.circular(15.w),
|
|
||||||
bottomRight: Radius.circular(15.w),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 10.w,
|
|
||||||
vertical: 4.w,
|
|
||||||
),
|
|
||||||
margin: EdgeInsetsDirectional.only(
|
|
||||||
start: 54.w,
|
|
||||||
).copyWith(bottom: 8.w),
|
|
||||||
child: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: SCAppLocalizations.of(context)!.sendTo,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13.sp,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(text: " "),
|
|
||||||
TextSpan(
|
|
||||||
text: _giftTargetName(context),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13.sp,
|
|
||||||
color: SocialChatTheme.primaryColor,
|
|
||||||
),
|
|
||||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
|
||||||
),
|
|
||||||
TextSpan(text: " "),
|
|
||||||
WidgetSpan(
|
|
||||||
child: netImage(
|
|
||||||
url: widget.msg.gift?.giftPhoto ?? "",
|
|
||||||
width: 22.w,
|
|
||||||
height: 22.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(text: " "),
|
|
||||||
TextSpan(
|
|
||||||
text: "x${widget.msg.number}",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13.sp,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGiftMessageBubble(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black38,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(15.w),
|
||||||
|
bottomLeft: Radius.circular(15.w),
|
||||||
|
bottomRight: Radius.circular(15.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 0).copyWith(bottom: 8.w),
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: SCAppLocalizations.of(context)!.sendTo,
|
||||||
|
style: TextStyle(fontSize: 13.sp, color: Colors.white),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
TextSpan(
|
||||||
|
text: _giftTargetName(context),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.sp,
|
||||||
|
color: SocialChatTheme.primaryColor,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
WidgetSpan(
|
||||||
|
child: netImage(
|
||||||
|
url: widget.msg.gift?.giftPhoto ?? "",
|
||||||
|
width: 22.w,
|
||||||
|
height: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
TextSpan(
|
||||||
|
text: "x${widget.msg.number}",
|
||||||
|
style: TextStyle(fontSize: 13.sp, color: Colors.white),
|
||||||
|
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1625,19 +1654,12 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
if (path.isEmpty) {
|
if (path.isEmpty) {
|
||||||
return SizedBox(width: width, height: height);
|
return SizedBox(width: width, height: height);
|
||||||
}
|
}
|
||||||
if (_isLocalAsset(path)) {
|
return RoomEmojiAssetImage(
|
||||||
return RoomEmojiAssetImage(
|
key: ValueKey(path),
|
||||||
key: ValueKey(path),
|
asset: path,
|
||||||
asset: path,
|
width: width,
|
||||||
width: width,
|
height: height,
|
||||||
height: height,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
return netImage(url: path, width: width, height: height);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isLocalAsset(String path) {
|
|
||||||
return path.startsWith("assets/") || path.startsWith("sc_images/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDiceMsg(BuildContext context) {
|
Widget _buildDiceMsg(BuildContext context) {
|
||||||
|
|||||||
94
lib/ui_kit/widgets/sc_home_shell_background.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
|
const Color scHomeShellBackgroundColor = Color(0xFF051711);
|
||||||
|
const Color scHomeShellAccentColor = Color(0xFF18F2B1);
|
||||||
|
const Color scHomeShellGoldColor = Color(0xFFE9B455);
|
||||||
|
const LinearGradient scHomeShellGoldTextGradient = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFFFCFBD9), Color(0xFFDC9D3A)],
|
||||||
|
);
|
||||||
|
const String scHomeShellBackgroundAsset =
|
||||||
|
'sc_images/index/sc_home_shell_bg.png';
|
||||||
|
|
||||||
|
class SCHomeShellBackground extends StatelessWidget {
|
||||||
|
const SCHomeShellBackground({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: const BoxDecoration(color: scHomeShellBackgroundColor),
|
||||||
|
child: Image.asset(
|
||||||
|
scHomeShellBackgroundAsset,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) =>
|
||||||
|
const ColoredBox(color: scHomeShellBackgroundColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SCHomeShellGradientText extends StatelessWidget {
|
||||||
|
const SCHomeShellGradientText(
|
||||||
|
this.text, {
|
||||||
|
super.key,
|
||||||
|
required this.style,
|
||||||
|
this.maxLines,
|
||||||
|
this.overflow,
|
||||||
|
this.textAlign,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final TextStyle style;
|
||||||
|
final int? maxLines;
|
||||||
|
final TextOverflow? overflow;
|
||||||
|
final TextAlign? textAlign;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gradientText = ShaderMask(
|
||||||
|
blendMode: BlendMode.srcIn,
|
||||||
|
shaderCallback: scHomeShellGoldTextGradient.createShader,
|
||||||
|
child: _buildText(style.copyWith(color: Colors.white, shadows: null)),
|
||||||
|
);
|
||||||
|
if (style.shadows == null || style.shadows!.isEmpty) {
|
||||||
|
return gradientText;
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
ExcludeSemantics(
|
||||||
|
child: _buildText(style.copyWith(color: Colors.transparent)),
|
||||||
|
),
|
||||||
|
gradientText,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildText(TextStyle textStyle) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
maxLines: maxLines,
|
||||||
|
overflow: overflow,
|
||||||
|
textAlign: textAlign,
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle scHomeShellTitleStyle({double? fontSize}) {
|
||||||
|
return TextStyle(
|
||||||
|
color: scHomeShellGoldColor,
|
||||||
|
fontSize: (fontSize ?? 28).sp,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
shadows: const [
|
||||||
|
Shadow(color: Color(0xB3000000), offset: Offset(0, 1), blurRadius: 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
319
local_packages/extended_text_field-16.0.2-patched/CHANGELOG.md
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
## 16.0.2
|
||||||
|
|
||||||
|
* Fix issue that the caret offset is not right after pinyin(composing) is completed on windows desktop(#255)
|
||||||
|
|
||||||
|
## 16.0.1
|
||||||
|
|
||||||
|
* Fix issue that context menu click didn't work on desktop(#250)
|
||||||
|
|
||||||
|
## 16.0.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.24.0
|
||||||
|
|
||||||
|
## 15.0.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.22.0
|
||||||
|
|
||||||
|
## 14.0.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.19.0
|
||||||
|
* Fix wrong postion of Magnifier
|
||||||
|
|
||||||
|
## 13.0.1
|
||||||
|
|
||||||
|
* Update readme about HarmonyOS
|
||||||
|
|
||||||
|
## 13.0.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.16.0 (#229)
|
||||||
|
* Fix wrong caret position (#224,#226)
|
||||||
|
|
||||||
|
## 12.1.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.13.0
|
||||||
|
|
||||||
|
## 12.0.1
|
||||||
|
|
||||||
|
* Fix issue that wrong cursor position on macos. (https://github.com/fluttercandies/extended_text_field/issues/210)
|
||||||
|
|
||||||
|
## 12.0.0
|
||||||
|
|
||||||
|
* Migrate to Flutter 3.10.0
|
||||||
|
* Refactoring codes and sync codes from 3.10.0
|
||||||
|
* Breaking change:
|
||||||
|
Remove [ExtendedText.textSelectionGestureDetectorBuilder],[ExtendedText.shouldShowSelectionHandles]
|
||||||
|
* Add ExtendedSelectableText
|
||||||
|
|
||||||
|
## 11.0.1
|
||||||
|
|
||||||
|
* fix issue on ios after flutter version 3.7.0. #191 #198
|
||||||
|
|
||||||
|
## 11.0.0
|
||||||
|
|
||||||
|
* Migrate to 3.7.0
|
||||||
|
|
||||||
|
## 10.2.0
|
||||||
|
|
||||||
|
* Add TextInputBindingMixin to prevent system keyboard show.
|
||||||
|
* Add No SystemKeyboard demo
|
||||||
|
|
||||||
|
## 10.1.1
|
||||||
|
|
||||||
|
* Fix issue selection not right #172
|
||||||
|
|
||||||
|
## 10.1.0
|
||||||
|
|
||||||
|
* Migrate to 3.0.0
|
||||||
|
* Support Scribble Handwriting for iPads
|
||||||
|
|
||||||
|
## 10.0.1
|
||||||
|
|
||||||
|
* Public ExtendedTextFieldState and add bringIntoView method to support jump to caret when insert text with TextEditingController
|
||||||
|
|
||||||
|
## 10.0.0
|
||||||
|
|
||||||
|
* Migrate to 2.10.0.
|
||||||
|
* Add shouldShowSelectionHandles and textSelectionGestureDetectorBuilder call back to define the behavior of handles and toolbar.
|
||||||
|
* Shortcut support for web and desktop.
|
||||||
|
|
||||||
|
## 9.0.3
|
||||||
|
|
||||||
|
* Fix hittest is not right #131
|
||||||
|
|
||||||
|
## 9.0.2
|
||||||
|
|
||||||
|
* Fix selectionWidthStyle and selectionHeightStyle are not working.
|
||||||
|
|
||||||
|
## 9.0.1
|
||||||
|
|
||||||
|
* Support to use keyboard move cursor for SpecialInlineSpan. #135
|
||||||
|
* Fix issue that backspace delete two chars. #141
|
||||||
|
|
||||||
|
## 9.0.0
|
||||||
|
|
||||||
|
* Migrate to 2.8
|
||||||
|
|
||||||
|
## 8.0.0
|
||||||
|
|
||||||
|
* Add [SpecialTextSpan.mouseCursor], [SpecialTextSpan.onEnter] and [SpecialTextSpan.onExit].
|
||||||
|
* merge code from 2.2.0
|
||||||
|
|
||||||
|
## 7.0.1
|
||||||
|
|
||||||
|
* Fix issue that composing is not updated.#122
|
||||||
|
|
||||||
|
## 7.0.0
|
||||||
|
|
||||||
|
* Breaking change: [SpecialText.getContent] is not include endflag now.(please check if you call getContent and your endflag length is more than 1)
|
||||||
|
* Fix demo manualDelete error #120
|
||||||
|
|
||||||
|
## 6.0.1
|
||||||
|
|
||||||
|
* Fix issue that toolbar is not shown when double tap
|
||||||
|
* Fix throw exception when selectWordAtOffset
|
||||||
|
|
||||||
|
## 6.0.0
|
||||||
|
|
||||||
|
* Support null-safety
|
||||||
|
|
||||||
|
## 5.0.4
|
||||||
|
|
||||||
|
* Fix toolbar is not show after some behaviour #107
|
||||||
|
|
||||||
|
## 5.0.3
|
||||||
|
|
||||||
|
* Fix miss TextStyle for specialTextSpanBuilder #89
|
||||||
|
|
||||||
|
## 5.0.2
|
||||||
|
|
||||||
|
* Fix wrong position of caret
|
||||||
|
|
||||||
|
## 5.0.1
|
||||||
|
|
||||||
|
* change handleSpecialText to hasSpecialInlineSpanBase(extended_text_library)
|
||||||
|
* add hasPlaceholderSpan(extended_text_library)
|
||||||
|
|
||||||
|
## 5.0.0
|
||||||
|
|
||||||
|
* Merge from Flutter v1.22
|
||||||
|
* Support cursorHeight, onAppPrivateCommand, restorationId
|
||||||
|
|
||||||
|
## 4.0.0
|
||||||
|
|
||||||
|
* Merge from Flutter v1.20
|
||||||
|
* Support Autofill
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
* Fix issue that text is clipped when maxLine is 1 and width is more than maxWidth.(#67,#76)
|
||||||
|
* Fix issue that handles are not shown when the height of TextStyle is big than 1.0.(#49)
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
* Breaking change: fix typos OverflowWidget.
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
* Breaking change: extended_text_library has changed to support OverFlowWidget ExtendedText
|
||||||
|
* Fix textSelectionControls is not working.
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
|
||||||
|
* Fix wrong calculation about selection handles.
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
* Merge code from 1.17.0
|
||||||
|
* Fix analysis_options
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Fix error caret offset on ios.
|
||||||
|
|
||||||
|
## 0.4.9
|
||||||
|
|
||||||
|
* Fix error about TargetPlatform.macOS
|
||||||
|
|
||||||
|
## 0.4.8
|
||||||
|
|
||||||
|
* Fix issue that the cursor returns to the top when deleting quickly in Multi-line text
|
||||||
|
* Fix issue that toolbar will not show if autofocus is true when longpress
|
||||||
|
|
||||||
|
## 0.4.7
|
||||||
|
|
||||||
|
* Codes base on 1.12.13+hotfix.5
|
||||||
|
* Set limitation of flutter sdk >=1.12.13 <1.12.16
|
||||||
|
* Add demo that how to delete text with code
|
||||||
|
* Fix issue that not showing text while entering text from keyboard
|
||||||
|
|
||||||
|
## 0.4.6
|
||||||
|
|
||||||
|
* Remove TargetPlatform.macOS
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
* Fix build error for flutter sdk 1.12
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
* Fix wrong caret hegiht and postion
|
||||||
|
* Make Ios/Android caret the same height
|
||||||
|
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
* Fix kMinInteractiveSize is missing in high version of flutter
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
* Support custom selection toolbar and handles
|
||||||
|
* Improve codes about selection overlay
|
||||||
|
* Select all SpecialTextSpan(which deleteAll is true) when double tap or long tap
|
||||||
|
* Support WidgetSpan hitTest
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
* Fix issue that type 'ImageSpan' is not a subtype of type 'textSpan'(https://github.com/fluttercandies/extended_text_field/issues/13)
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* Fix issue tgat WidgetSpan wrong offset(https://github.com/fluttercandies/extended_text_field/issues/11)
|
||||||
|
* Fix wrong caret offset
|
||||||
|
|
||||||
|
## 0.3.9
|
||||||
|
|
||||||
|
* Improve codes base on v1.7.8
|
||||||
|
* Support WidgetSpan (ExtendedWidgetSpan)
|
||||||
|
|
||||||
|
## 0.3.7
|
||||||
|
|
||||||
|
* Update extended_text_library
|
||||||
|
|
||||||
|
## 0.3.4
|
||||||
|
|
||||||
|
* Remove un-used codes in extended_text_selection
|
||||||
|
|
||||||
|
## 0.3.3
|
||||||
|
|
||||||
|
* Update extended_text_library
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
* Update path_provider 1.1.0
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* Uncomment getFullHeightForCaret method for 1.5.4-hotfix.2
|
||||||
|
* Corret selection handles visibility for _updateSelectionExtentsVisibility method
|
||||||
|
|
||||||
|
## 0.2.8
|
||||||
|
|
||||||
|
* Corret selection handles position for image textspan
|
||||||
|
* StrutStyle strutStyle is obsoleted, it will lead to bugs for image span size.
|
||||||
|
|
||||||
|
## 0.2.7
|
||||||
|
|
||||||
|
* Fix selection handles blinking
|
||||||
|
|
||||||
|
## 0.2.6
|
||||||
|
|
||||||
|
* Take care when TextSpan children is null
|
||||||
|
|
||||||
|
## 0.2.5
|
||||||
|
|
||||||
|
* Update extended_text_library
|
||||||
|
1.Remove caretIn parameter(SpecialTextSpan)
|
||||||
|
2.DeleteAll parameter has the same effect as caretIn parameter(SpecialTextSpan)
|
||||||
|
|
||||||
|
## 0.2.4
|
||||||
|
|
||||||
|
* Fix caret position about image span
|
||||||
|
* Add caretIn parameter(whether caret can move into special text for SpecialTextSpan(like a image span or @xxxx)) for SpecialTextSpan
|
||||||
|
|
||||||
|
## 0.2.3
|
||||||
|
|
||||||
|
* Disabled informationCollector to keep backwards compatibility for now (ExtendedNetworkImageProvider)
|
||||||
|
|
||||||
|
## 0.2.2
|
||||||
|
|
||||||
|
* Fix caret position for last one image span
|
||||||
|
* Add image text demo
|
||||||
|
* Fix position for specialTex
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
* Fix caret position for image span
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
* Only iterate textSpan.children to find SpecialTextSpan
|
||||||
|
|
||||||
|
## 0.1.9
|
||||||
|
|
||||||
|
* Add BackgroundTextSpan, support to paint custom background
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
* Handle TextEditingValue's composing
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
* Improve codes to avoid unnecessary computation
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
* Override compareTo method in SpecialTextSpan and ImageSpan to
|
||||||
|
Fix issue that image span or special text span was error rendering
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
* Update limitation
|
||||||
|
* Improve codes
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
* Update limitation
|
||||||
|
* Improve codes
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
* Support special text amd inline image
|
||||||
@ -0,0 +1 @@
|
|||||||
|
* @zmtzawqlp
|
||||||
21
local_packages/extended_text_field-16.0.2-patched/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 zmtzawqlp
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
616
local_packages/extended_text_field-16.0.2-patched/README-ZH.md
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
# extended_text_field
|
||||||
|
|
||||||
|
[](https://pub.dartlang.org/packages/extended_text_field) [](https://github.com/fluttercandies/extended_text_field/stargazers) [](https://github.com/fluttercandies/extended_text_field/network) [](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [](https://github.com/fluttercandies/extended_text_field/issues) <a target="_blank" href="https://jq.qq.com/?_wv=1027&k=5bcc0gy"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="flutter-candies" title="flutter-candies"></a>
|
||||||
|
|
||||||
|
文档语言: [English](README.md) | 中文简体
|
||||||
|
|
||||||
|
官方输入框的扩展组件,支持图片,@某人,自定义文字背景。也支持自定义菜单和选择器。
|
||||||
|
|
||||||
|
[ExtendedTextField 在线 Demo](https://fluttercandies.github.io/extended_text_field/)
|
||||||
|
|
||||||
|
ExtendedTextField 是 Flutter 官方 TextField 的三方扩展库,主要扩展功能如下:
|
||||||
|
|
||||||
|
| 功能 | ExtendedTextField | TextField |
|
||||||
|
|-----------------------------------------|---------------------------------------------------------|----------------------------------------------------------|
|
||||||
|
| 图文混合 | 支持,可以实现图文混合显示 | 仅支持显示文本,但在选择文本时会出现问题 |
|
||||||
|
| 支持复制真实值 | 支持,可以复制出文本的真实值 | 不支持 |
|
||||||
|
| 根据文本格式快速构建富文本 | 支持,可以根据文本格式快速构建富文本 | 不支持 |
|
||||||
|
|
||||||
|
> 已支持 `HarmonyOS`. 请使用最新的带有 `ohos` 标志的版本. 你可以在 `Versions` 签查找.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
extended_text_field: 11.0.1-ohos
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- [extended\_text\_field](#extended_text_field)
|
||||||
|
- [限制](#限制)
|
||||||
|
- [特殊文本](#特殊文本)
|
||||||
|
- [创建特殊文本](#创建特殊文本)
|
||||||
|
- [特殊文本Builder](#特殊文本builder)
|
||||||
|
- [图片](#图片)
|
||||||
|
- [ImageSpan](#imagespan)
|
||||||
|
- [缓存图片](#缓存图片)
|
||||||
|
- [文本选择控制器](#文本选择控制器)
|
||||||
|
- [WidgetSpan](#widgetspan)
|
||||||
|
- [阻止系统键盘](#阻止系统键盘)
|
||||||
|
- [TextInputBindingMixin](#textinputbindingmixin)
|
||||||
|
- [TextInputFocusNode](#textinputfocusnode)
|
||||||
|
- [CustomKeyboard](#customkeyboard)
|
||||||
|
- [☕️Buy me a coffee](#️buy-me-a-coffee)
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
- 不支持TextDirection.rtl,从右向左.
|
||||||
|
|
||||||
|
- 不支持obscureText为true.
|
||||||
|
|
||||||
|
## 特殊文本
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 创建特殊文本
|
||||||
|
|
||||||
|
extended_text 帮助将字符串文本快速转换为特殊的TextSpan
|
||||||
|
|
||||||
|
下面的例子告诉你怎么创建一个@xxx
|
||||||
|
|
||||||
|
具体思路是对字符串进行进栈遍历,通过判断flag来判定是否是一个特殊字符。
|
||||||
|
例子:@zmtzawqlp ,以@开头并且以空格结束,我们就认为它是一个@的特殊文本
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class AtText extends SpecialText {
|
||||||
|
static const String flag = "@";
|
||||||
|
final int start;
|
||||||
|
|
||||||
|
/// whether show background for @somebody
|
||||||
|
final bool showAtBackground;
|
||||||
|
|
||||||
|
AtText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
|
||||||
|
{this.showAtBackground: false, this.start})
|
||||||
|
: super(
|
||||||
|
flag,
|
||||||
|
" ",
|
||||||
|
textStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan finishText() {
|
||||||
|
TextStyle textStyle =
|
||||||
|
this.textStyle?.copyWith(color: Colors.blue, fontSize: 16.0);
|
||||||
|
|
||||||
|
final String atText = toString();
|
||||||
|
|
||||||
|
return showAtBackground
|
||||||
|
? BackgroundTextSpan(
|
||||||
|
background: Paint()..color = Colors.blue.withOpacity(0.15),
|
||||||
|
text: atText,
|
||||||
|
actualText: atText,
|
||||||
|
start: start,
|
||||||
|
|
||||||
|
///caret can move into special text
|
||||||
|
deleteAll: true,
|
||||||
|
style: textStyle,
|
||||||
|
recognizer: (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (onTap != null) onTap(atText);
|
||||||
|
}))
|
||||||
|
: SpecialTextSpan(
|
||||||
|
text: atText,
|
||||||
|
actualText: atText,
|
||||||
|
start: start,
|
||||||
|
style: textStyle,
|
||||||
|
recognizer: (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (onTap != null) onTap(atText);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特殊文本Builder
|
||||||
|
|
||||||
|
创建属于你自己规则的Builder,上面说了你可以继承SpecialText来定义各种各样的特殊文本。
|
||||||
|
- build 方法中,是通过具体思路是对字符串进行进栈遍历,通过判断flag来判定是否是一个特殊文本。
|
||||||
|
感兴趣的,可以看一下SpecialTextSpanBuilder里面build方法的实现,当然你也可以写出属于自己的build逻辑
|
||||||
|
- createSpecialText 通过判断flag来判定是否是一个特殊文本
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||||
|
/// whether show background for @somebody
|
||||||
|
final bool showAtBackground;
|
||||||
|
final BuilderType type;
|
||||||
|
MySpecialTextSpanBuilder(
|
||||||
|
{this.showAtBackground: false, this.type: BuilderType.extendedText});
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan build(String data, {TextStyle textStyle, onTap}) {
|
||||||
|
var textSpan = super.build(data, textStyle: textStyle, onTap: onTap);
|
||||||
|
return textSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SpecialText createSpecialText(String flag,
|
||||||
|
{TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {
|
||||||
|
if (flag == null || flag == "") return null;
|
||||||
|
|
||||||
|
///index is end index of start flag, so text start index should be index-(flag.length-1)
|
||||||
|
if (isStart(flag, AtText.flag)) {
|
||||||
|
return AtText(textStyle, onTap,
|
||||||
|
start: index - (AtText.flag.length - 1),
|
||||||
|
showAtBackground: showAtBackground,
|
||||||
|
type: type);
|
||||||
|
} else if (isStart(flag, EmojiText.flag)) {
|
||||||
|
return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));
|
||||||
|
} else if (isStart(flag, DollarText.flag)) {
|
||||||
|
return DollarText(textStyle, onTap,
|
||||||
|
start: index - (DollarText.flag.length - 1), type: type);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
其实你也不是一定要用这套代码将字符串转换为TextSpan,你可以有自己的方法,给最后的TextSpan就可以了。
|
||||||
|
|
||||||
|
## 图片
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### ImageSpan
|
||||||
|
|
||||||
|
使用ImageSpan 展示图片
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ImageSpan(
|
||||||
|
ImageProvider image, {
|
||||||
|
Key key,
|
||||||
|
@required double imageWidth,
|
||||||
|
@required double imageHeight,
|
||||||
|
EdgeInsets margin,
|
||||||
|
int start: 0,
|
||||||
|
ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,
|
||||||
|
String actualText,
|
||||||
|
TextBaseline baseline,
|
||||||
|
TextStyle style,
|
||||||
|
BoxFit fit: BoxFit.scaleDown,
|
||||||
|
ImageLoadingBuilder loadingBuilder,
|
||||||
|
ImageFrameBuilder frameBuilder,
|
||||||
|
String semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
Color color,
|
||||||
|
BlendMode colorBlendMode,
|
||||||
|
AlignmentGeometry imageAlignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Rect centerSlice,
|
||||||
|
bool matchTextDirection = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
})
|
||||||
|
|
||||||
|
ImageSpan(AssetImage("xxx.jpg"),
|
||||||
|
imageWidth: size,
|
||||||
|
imageHeight: size,
|
||||||
|
margin: EdgeInsets.only(left: 2.0, bottom: 0.0, right: 2.0));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 描述 | 默认 |
|
||||||
|
| ----------- | ----------------------------------------------------------------- | ---------------- |
|
||||||
|
| image | 图片展示的Provider(ImageProvider) | - |
|
||||||
|
| imageWidth | 宽度,不包括 margin | 必填 |
|
||||||
|
| imageHeight | 高度,不包括 margin | 必填 |
|
||||||
|
| margin | 图片的margin | - |
|
||||||
|
| actualText | 真实的文本,当你开启文本选择功能的时候,必须设置,比如图片"\[love\] | 空占位符'\uFFFC' |
|
||||||
|
| start | 在文本字符串中的开始位置,当你开启文本选择功能的时候,必须设置 | 0 |
|
||||||
|
|
||||||
|
### 缓存图片
|
||||||
|
|
||||||
|
你可以用ExtendedNetworkImageProvider来缓存文本中的图片,使用clearDiskCachedImages方法来清掉本地缓存
|
||||||
|
|
||||||
|
引入 extended_image_library
|
||||||
|
|
||||||
|
```dart
|
||||||
|
dependencies:
|
||||||
|
extended_image_library: ^0.1.4
|
||||||
|
```
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ExtendedNetworkImageProvider(
|
||||||
|
this.url, {
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.headers,
|
||||||
|
this.cache: false,
|
||||||
|
this.retries = 3,
|
||||||
|
this.timeLimit,
|
||||||
|
this.timeRetry = const Duration(milliseconds: 100),
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
}) : assert(url != null),
|
||||||
|
assert(scale != null),
|
||||||
|
cancelToken = cancelToken ?? CancellationToken();
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 描述 | 默认 |
|
||||||
|
| ----------- | ------------------- | ------------------- |
|
||||||
|
| url | 网络请求地址 | required |
|
||||||
|
| scale | ImageInfo中的scale | 1.0 |
|
||||||
|
| headers | HttpClient的headers | - |
|
||||||
|
| cache | 是否缓存到本地 | false |
|
||||||
|
| retries | 请求尝试次数 | 3 |
|
||||||
|
| timeLimit | 请求超时 | - |
|
||||||
|
| timeRetry | 请求重试间隔 | milliseconds: 100 |
|
||||||
|
| cancelToken | 用于取消请求的Token | CancellationToken() |
|
||||||
|
|
||||||
|
```dart
|
||||||
|
/// Clear the disk cache directory then return if it succeed.
|
||||||
|
/// <param name="duration">timespan to compute whether file has expired or not</param>
|
||||||
|
Future<bool> clearDiskCachedImages({Duration duration}) async
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文本选择控制器
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
提供了默认的控制器MaterialExtendedTextSelectionControls/CupertinoExtendedTextSelectionControls
|
||||||
|
|
||||||
|
通过重写 [ExtendedTextField.extendedContextMenuBuilder] 和 [TextSelectionControls] 来自定义菜单和选择器。
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const double _kHandleSize = 22.0;
|
||||||
|
|
||||||
|
/// Android Material styled text selection controls.
|
||||||
|
class MyTextSelectionControls extends TextSelectionControls
|
||||||
|
with TextSelectionHandleControls {
|
||||||
|
static Widget defaultContextMenuBuilder(
|
||||||
|
BuildContext context, ExtendedEditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
...editableTextState.contextMenuButtonItems,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
'mailto:zmtzawqlp@live.com?subject=extended_text_share&body=${editableTextState.textEditingValue.text}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editableTextState.hideToolbar(true);
|
||||||
|
editableTextState.textEditingValue
|
||||||
|
.copyWith(selection: const TextSelection.collapsed(offset: 0));
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.custom,
|
||||||
|
label: 'like',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
);
|
||||||
|
// return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
// editableTextState: editableTextState,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the Material handle.
|
||||||
|
@override
|
||||||
|
Size getHandleSize(double textLineHeight) =>
|
||||||
|
const Size(_kHandleSize, _kHandleSize);
|
||||||
|
|
||||||
|
/// Builder for material-style text selection handles.
|
||||||
|
@override
|
||||||
|
Widget buildHandle(
|
||||||
|
BuildContext context, TextSelectionHandleType type, double textLineHeight,
|
||||||
|
[VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
|
||||||
|
final Widget handle = SizedBox(
|
||||||
|
width: _kHandleSize,
|
||||||
|
height: _kHandleSize,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/40.png',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// [handle] is a circle, with a rectangle in the top left quadrant of that
|
||||||
|
// circle (an onion pointing to 10:30). We rotate [handle] to point
|
||||||
|
// straight up or up-right depending on the handle type.
|
||||||
|
switch (type) {
|
||||||
|
case TextSelectionHandleType.left: // points up-right
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: math.pi / 4.0,
|
||||||
|
child: handle,
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.right: // points up-left
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: -math.pi / 4.0,
|
||||||
|
child: handle,
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.collapsed: // points up
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets anchor for material-style text selection handles.
|
||||||
|
///
|
||||||
|
/// See [TextSelectionControls.getHandleAnchor].
|
||||||
|
@override
|
||||||
|
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,
|
||||||
|
[double? startGlyphHeight, double? endGlyphHeight]) {
|
||||||
|
switch (type) {
|
||||||
|
case TextSelectionHandleType.left:
|
||||||
|
return const Offset(_kHandleSize, 0);
|
||||||
|
case TextSelectionHandleType.right:
|
||||||
|
return Offset.zero;
|
||||||
|
default:
|
||||||
|
return const Offset(_kHandleSize / 2, -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||||
|
// Android allows SelectAll when selection is not collapsed, unless
|
||||||
|
// everything has already been selected.
|
||||||
|
final TextEditingValue value = delegate.textEditingValue;
|
||||||
|
return delegate.selectAllEnabled &&
|
||||||
|
value.text.isNotEmpty &&
|
||||||
|
!(value.selection.start == 0 &&
|
||||||
|
value.selection.end == value.text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## WidgetSpan
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
ExtendedWidgetSpan 支持选择以及hitTest, 所以你可以在输入框中加入任何的widget。
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class EmailText extends SpecialText {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final int start;
|
||||||
|
final BuildContext context;
|
||||||
|
EmailText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
|
||||||
|
{this.start, this.controller, this.context, String startFlag})
|
||||||
|
: super(startFlag, " ", textStyle, onTap: onTap);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isEnd(String value) {
|
||||||
|
var index = value.indexOf("@");
|
||||||
|
var index1 = value.indexOf(".");
|
||||||
|
|
||||||
|
return index >= 0 &&
|
||||||
|
index1 >= 0 &&
|
||||||
|
index1 > index + 1 &&
|
||||||
|
super.isEnd(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan finishText() {
|
||||||
|
final String text = toString();
|
||||||
|
|
||||||
|
return ExtendedWidgetSpan(
|
||||||
|
actualText: text,
|
||||||
|
start: start,
|
||||||
|
alignment: ui.PlaceholderAlignment.middle,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 5.0, top: 2.0, bottom: 2.0),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(5.0),
|
||||||
|
color: Colors.orange,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
text.trim(),
|
||||||
|
//style: textStyle?.copyWith(color: Colors.orange),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 5.0,
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 15.0,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: controller.text
|
||||||
|
.replaceRange(start, start + text.length, ""),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: start)));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (c) {
|
||||||
|
TextEditingController textEditingController =
|
||||||
|
TextEditingController()..text = text.trim();
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: textEditingController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
suffixIcon: FlatButton(
|
||||||
|
child: Text("OK"),
|
||||||
|
onPressed: () {
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: controller.text.replaceRange(
|
||||||
|
start,
|
||||||
|
start + text.length,
|
||||||
|
textEditingController.text + " "),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(
|
||||||
|
offset: start +
|
||||||
|
(textEditingController.text + " ")
|
||||||
|
.length)));
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
deleteAll: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 阻止系统键盘
|
||||||
|
|
||||||
|
我们不需要代码侵入到 [ExtendedTextField] 或者 [TextField] 当中, 就可以阻止系统键盘弹出,
|
||||||
|
|
||||||
|
### TextInputBindingMixin
|
||||||
|
|
||||||
|
我们通过阻止 Flutter Framework 发送 `TextInput.show` 到 Flutter 引擎来阻止系统键盘弹出
|
||||||
|
|
||||||
|
你可以直接使用 [TextInputBinding].
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
void main() {
|
||||||
|
TextInputBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
或者你如果有其他的 `binding`,你可以这样。
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
YourBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
或者你需要重载 `ignoreTextInputShow` 方法,你可以这样。
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
class YourBinding extends TextInputBinding {
|
||||||
|
@override
|
||||||
|
// ignore: unnecessary_overrides
|
||||||
|
bool ignoreTextInputShow() {
|
||||||
|
// you can override it base on your case
|
||||||
|
// if NoKeyboardFocusNode is not enough
|
||||||
|
return super.ignoreTextInputShow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
YourBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TextInputFocusNode
|
||||||
|
|
||||||
|
把 [TextInputFocusNode] 传递给 [ExtendedTextField] 或者 [TextField]。
|
||||||
|
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
final TextInputFocusNode _focusNode = TextInputFocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ExtendedTextField(
|
||||||
|
// request keyboard if need
|
||||||
|
focusNode: _focusNode..debugLabel = 'ExtendedTextField',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
// request keyboard if need
|
||||||
|
focusNode: _focusNode..debugLabel = 'CustomTextField',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
我们通过当前的 `FocusNode` 是否是 [TextInputFocusNode],来决定是否阻止系统键盘弹出的。
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
final FocusNode? focus = FocusManager.instance.primaryFocus;
|
||||||
|
if (focus != null &&
|
||||||
|
focus is TextInputFocusNode &&
|
||||||
|
focus.ignoreSystemKeyboardShow) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### CustomKeyboard
|
||||||
|
|
||||||
|
你可以通过当前焦点的变化的时候,来显示或者隐藏自定义的键盘。
|
||||||
|
|
||||||
|
当你的自定义键盘可以关闭而不让焦点失去,你应该在 [ExtendedTextField] 或者 [TextField]
|
||||||
|
的 `onTap` 事件中,再次判断键盘是否显示。
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode.addListener(_handleFocusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextFiledTap() {
|
||||||
|
if (_bottomSheetController == null) {
|
||||||
|
_handleFocusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleFocusChanged() {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
// just demo, you can define your custom keyboard as you want
|
||||||
|
_bottomSheetController = showBottomSheet<void>(
|
||||||
|
context: FocusManager.instance.primaryFocus!.context!,
|
||||||
|
// set false, if don't want to drag to close custom keyboard
|
||||||
|
enableDrag: true,
|
||||||
|
builder: (BuildContext b) {
|
||||||
|
// your custom keyboard
|
||||||
|
return Container();
|
||||||
|
});
|
||||||
|
// maybe drag close
|
||||||
|
_bottomSheetController?.closed.whenComplete(() {
|
||||||
|
_bottomSheetController = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_bottomSheetController?.close();
|
||||||
|
_bottomSheetController = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_handleFocusChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
查看 [完整的例子](https://github.com/fluttercandies/extended_text_field/tree/master/example/lib/pages/simple/no_keyboard.dart)
|
||||||
|
|
||||||
|
## ☕️Buy me a coffee
|
||||||
|
|
||||||
|

|
||||||
605
local_packages/extended_text_field-16.0.2-patched/README.md
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
# extended_text_field
|
||||||
|
|
||||||
|
[](https://pub.dartlang.org/packages/extended_text_field) [](https://github.com/fluttercandies/extended_text_field/stargazers) [](https://github.com/fluttercandies/extended_text_field/network) [](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [](https://github.com/fluttercandies/extended_text_field/issues) <a target="_blank" href="https://jq.qq.com/?_wv=1027&k=5bcc0gy"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="flutter-candies" title="flutter-candies"></a>
|
||||||
|
|
||||||
|
Language: English | [中文简体](README-ZH.md)
|
||||||
|
|
||||||
|
Extended official text field to build special text like inline image, @somebody, custom background etc quickly.It also support to build custom seleciton toolbar and handles.
|
||||||
|
|
||||||
|
[Web demo for ExtendedTextField](https://fluttercandies.github.io/extended_text_field/)
|
||||||
|
|
||||||
|
ExtendedTextField is a third-party extension library for Flutter's official TextField component. The main extended features are as follows:
|
||||||
|
|
||||||
|
| Feature | ExtendedTextField | TextField |
|
||||||
|
|---------------------------------------|------------------------------------------------------|----------------------------------------------------|
|
||||||
|
| Inline images and text mixture | Supported, allows displaying inline images and mixed text | Only supports displaying text, but have issues with text selection |
|
||||||
|
| Copying the actual value | Supported, enables copying the actual value of the text | Not supported |
|
||||||
|
| Quick construction of rich text | Supported, enables quick construction of rich text based on text format | Not supported |
|
||||||
|
|
||||||
|
> `HarmonyOS` is supported. Please use the latest version which contains `ohos` tag. You can check it in `Versions` tab.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
extended_text_field: 11.0.1-ohos
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Please note that the translation provided above is based on the information you provided in the original text.
|
||||||
|
|
||||||
|
- [extended\_text\_field](#extended_text_field)
|
||||||
|
- [Limitation](#limitation)
|
||||||
|
- [Special Text](#special-text)
|
||||||
|
- [Create Special Text](#create-special-text)
|
||||||
|
- [SpecialTextSpanBuilder](#specialtextspanbuilder)
|
||||||
|
- [Image](#image)
|
||||||
|
- [ImageSpan](#imagespan)
|
||||||
|
- [Cache Image](#cache-image)
|
||||||
|
- [TextSelectionControls](#textselectioncontrols)
|
||||||
|
- [WidgetSpan](#widgetspan)
|
||||||
|
- [NoSystemKeyboard](#nosystemkeyboard)
|
||||||
|
- [TextInputBindingMixin](#textinputbindingmixin)
|
||||||
|
- [TextInputFocusNode](#textinputfocusnode)
|
||||||
|
|
||||||
|
## Limitation
|
||||||
|
|
||||||
|
- Not support: it won't handle special text when TextDirection.rtl.
|
||||||
|
|
||||||
|
Image position calculated by TextPainter is strange.
|
||||||
|
|
||||||
|
- Not support:it won't handle special text when obscureText is true.
|
||||||
|
|
||||||
|
## Special Text
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Create Special Text
|
||||||
|
|
||||||
|
extended text helps to convert your text to special textSpan quickly.
|
||||||
|
|
||||||
|
for example, follwing code show how to create @xxxx special textSpan.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class AtText extends SpecialText {
|
||||||
|
static const String flag = "@";
|
||||||
|
final int start;
|
||||||
|
|
||||||
|
/// whether show background for @somebody
|
||||||
|
final bool showAtBackground;
|
||||||
|
|
||||||
|
AtText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
|
||||||
|
{this.showAtBackground: false, this.start})
|
||||||
|
: super(
|
||||||
|
flag,
|
||||||
|
" ",
|
||||||
|
textStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan finishText() {
|
||||||
|
TextStyle textStyle =
|
||||||
|
this.textStyle?.copyWith(color: Colors.blue, fontSize: 16.0);
|
||||||
|
|
||||||
|
final String atText = toString();
|
||||||
|
|
||||||
|
return showAtBackground
|
||||||
|
? BackgroundTextSpan(
|
||||||
|
background: Paint()..color = Colors.blue.withOpacity(0.15),
|
||||||
|
text: atText,
|
||||||
|
actualText: atText,
|
||||||
|
start: start,
|
||||||
|
|
||||||
|
///caret can move into special text
|
||||||
|
deleteAll: true,
|
||||||
|
style: textStyle,
|
||||||
|
recognizer: (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (onTap != null) onTap(atText);
|
||||||
|
}))
|
||||||
|
: SpecialTextSpan(
|
||||||
|
text: atText,
|
||||||
|
actualText: atText,
|
||||||
|
start: start,
|
||||||
|
style: textStyle,
|
||||||
|
recognizer: (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (onTap != null) onTap(atText);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### SpecialTextSpanBuilder
|
||||||
|
|
||||||
|
create your SpecialTextSpanBuilder
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||||
|
/// whether show background for @somebody
|
||||||
|
final bool showAtBackground;
|
||||||
|
final BuilderType type;
|
||||||
|
MySpecialTextSpanBuilder(
|
||||||
|
{this.showAtBackground: false, this.type: BuilderType.extendedText});
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan build(String data, {TextStyle textStyle, onTap}) {
|
||||||
|
var textSpan = super.build(data, textStyle: textStyle, onTap: onTap);
|
||||||
|
return textSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SpecialText createSpecialText(String flag,
|
||||||
|
{TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {
|
||||||
|
if (flag == null || flag == "") return null;
|
||||||
|
|
||||||
|
///index is end index of start flag, so text start index should be index-(flag.length-1)
|
||||||
|
if (isStart(flag, AtText.flag)) {
|
||||||
|
return AtText(textStyle, onTap,
|
||||||
|
start: index - (AtText.flag.length - 1),
|
||||||
|
showAtBackground: showAtBackground,
|
||||||
|
type: type);
|
||||||
|
} else if (isStart(flag, EmojiText.flag)) {
|
||||||
|
return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));
|
||||||
|
} else if (isStart(flag, DollarText.flag)) {
|
||||||
|
return DollarText(textStyle, onTap,
|
||||||
|
start: index - (DollarText.flag.length - 1), type: type);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### ImageSpan
|
||||||
|
|
||||||
|
show inline image by using ImageSpan.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ImageSpan(
|
||||||
|
ImageProvider image, {
|
||||||
|
Key key,
|
||||||
|
@required double imageWidth,
|
||||||
|
@required double imageHeight,
|
||||||
|
EdgeInsets margin,
|
||||||
|
int start: 0,
|
||||||
|
ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,
|
||||||
|
String actualText,
|
||||||
|
TextBaseline baseline,
|
||||||
|
TextStyle style,
|
||||||
|
BoxFit fit: BoxFit.scaleDown,
|
||||||
|
ImageLoadingBuilder loadingBuilder,
|
||||||
|
ImageFrameBuilder frameBuilder,
|
||||||
|
String semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
Color color,
|
||||||
|
BlendMode colorBlendMode,
|
||||||
|
AlignmentGeometry imageAlignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Rect centerSlice,
|
||||||
|
bool matchTextDirection = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
})
|
||||||
|
|
||||||
|
ImageSpan(AssetImage("xxx.jpg"),
|
||||||
|
imageWidth: size,
|
||||||
|
imageHeight: size,
|
||||||
|
margin: EdgeInsets.only(left: 2.0, bottom: 0.0, right: 2.0));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| parameter | description | default |
|
||||||
|
| ----------- | ----------------------------------------------------------------------------- | -------- |
|
||||||
|
| image | The image to display(ImageProvider). | - |
|
||||||
|
| imageWidth | The width of image(not include margin) | required |
|
||||||
|
| imageHeight | The height of image(not include margin) | required |
|
||||||
|
| margin | The margin of image | - |
|
||||||
|
| actualText | Actual text, take care of it when enable selection,something likes "\[love\]" | '\uFFFC' |
|
||||||
|
| start | Start index of text,take care of it when enable selection. | 0 |
|
||||||
|
|
||||||
|
### Cache Image
|
||||||
|
|
||||||
|
if you want cache the network image, you can use ExtendedNetworkImageProvider and clear them with clearDiskCachedImages
|
||||||
|
|
||||||
|
import extended_image_library
|
||||||
|
|
||||||
|
```dart
|
||||||
|
dependencies:
|
||||||
|
extended_image_library: ^0.1.4
|
||||||
|
```
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ExtendedNetworkImageProvider(
|
||||||
|
this.url, {
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.headers,
|
||||||
|
this.cache: false,
|
||||||
|
this.retries = 3,
|
||||||
|
this.timeLimit,
|
||||||
|
this.timeRetry = const Duration(milliseconds: 100),
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
}) : assert(url != null),
|
||||||
|
assert(scale != null),
|
||||||
|
cancelToken = cancelToken ?? CancellationToken();
|
||||||
|
```
|
||||||
|
|
||||||
|
| parameter | description | default |
|
||||||
|
| ----------- | ------------------------------------------------------------------------------------- | ------------------- |
|
||||||
|
| url | The URL from which the image will be fetched. | required |
|
||||||
|
| scale | The scale to place in the [ImageInfo] object of the image. | 1.0 |
|
||||||
|
| headers | The HTTP headers that will be used with [HttpClient.get] to fetch image from network. | - |
|
||||||
|
| cache | whether cache image to local | false |
|
||||||
|
| retries | the time to retry to request | 3 |
|
||||||
|
| timeLimit | time limit to request image | - |
|
||||||
|
| timeRetry | the time duration to retry to request | milliseconds: 100 |
|
||||||
|
| cancelToken | token to cancel network request | CancellationToken() |
|
||||||
|
|
||||||
|
```dart
|
||||||
|
/// Clear the disk cache directory then return if it succeed.
|
||||||
|
/// <param name="duration">timespan to compute whether file has expired or not</param>
|
||||||
|
Future<bool> clearDiskCachedImages({Duration duration}) async
|
||||||
|
```
|
||||||
|
|
||||||
|
## TextSelectionControls
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
override [ExtendedTextField.extendedContextMenuBuilder] and [TextSelectionControls] to custom your toolbar widget or handle widget
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const double _kHandleSize = 22.0;
|
||||||
|
|
||||||
|
/// Android Material styled text selection controls.
|
||||||
|
class MyTextSelectionControls extends TextSelectionControls
|
||||||
|
with TextSelectionHandleControls {
|
||||||
|
static Widget defaultContextMenuBuilder(
|
||||||
|
BuildContext context, ExtendedEditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
...editableTextState.contextMenuButtonItems,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
'mailto:zmtzawqlp@live.com?subject=extended_text_share&body=${editableTextState.textEditingValue.text}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editableTextState.hideToolbar(true);
|
||||||
|
editableTextState.textEditingValue
|
||||||
|
.copyWith(selection: const TextSelection.collapsed(offset: 0));
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.custom,
|
||||||
|
label: 'like',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
);
|
||||||
|
// return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
// editableTextState: editableTextState,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the Material handle.
|
||||||
|
@override
|
||||||
|
Size getHandleSize(double textLineHeight) =>
|
||||||
|
const Size(_kHandleSize, _kHandleSize);
|
||||||
|
|
||||||
|
/// Builder for material-style text selection handles.
|
||||||
|
@override
|
||||||
|
Widget buildHandle(
|
||||||
|
BuildContext context, TextSelectionHandleType type, double textLineHeight,
|
||||||
|
[VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
|
||||||
|
final Widget handle = SizedBox(
|
||||||
|
width: _kHandleSize,
|
||||||
|
height: _kHandleSize,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/40.png',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// [handle] is a circle, with a rectangle in the top left quadrant of that
|
||||||
|
// circle (an onion pointing to 10:30). We rotate [handle] to point
|
||||||
|
// straight up or up-right depending on the handle type.
|
||||||
|
switch (type) {
|
||||||
|
case TextSelectionHandleType.left: // points up-right
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: math.pi / 4.0,
|
||||||
|
child: handle,
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.right: // points up-left
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: -math.pi / 4.0,
|
||||||
|
child: handle,
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.collapsed: // points up
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets anchor for material-style text selection handles.
|
||||||
|
///
|
||||||
|
/// See [TextSelectionControls.getHandleAnchor].
|
||||||
|
@override
|
||||||
|
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,
|
||||||
|
[double? startGlyphHeight, double? endGlyphHeight]) {
|
||||||
|
switch (type) {
|
||||||
|
case TextSelectionHandleType.left:
|
||||||
|
return const Offset(_kHandleSize, 0);
|
||||||
|
case TextSelectionHandleType.right:
|
||||||
|
return Offset.zero;
|
||||||
|
default:
|
||||||
|
return const Offset(_kHandleSize / 2, -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||||
|
// Android allows SelectAll when selection is not collapsed, unless
|
||||||
|
// everything has already been selected.
|
||||||
|
final TextEditingValue value = delegate.textEditingValue;
|
||||||
|
return delegate.selectAllEnabled &&
|
||||||
|
value.text.isNotEmpty &&
|
||||||
|
!(value.selection.start == 0 &&
|
||||||
|
value.selection.end == value.text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## WidgetSpan
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
support to select and hitTest ExtendedWidgetSpan, you can create any widget in ExtendedTextField.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class EmailText extends SpecialText {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final int start;
|
||||||
|
final BuildContext context;
|
||||||
|
EmailText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
|
||||||
|
{this.start, this.controller, this.context, String startFlag})
|
||||||
|
: super(startFlag, " ", textStyle, onTap: onTap);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isEnd(String value) {
|
||||||
|
var index = value.indexOf("@");
|
||||||
|
var index1 = value.indexOf(".");
|
||||||
|
|
||||||
|
return index >= 0 &&
|
||||||
|
index1 >= 0 &&
|
||||||
|
index1 > index + 1 &&
|
||||||
|
super.isEnd(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan finishText() {
|
||||||
|
final String text = toString();
|
||||||
|
|
||||||
|
return ExtendedWidgetSpan(
|
||||||
|
actualText: text,
|
||||||
|
start: start,
|
||||||
|
alignment: ui.PlaceholderAlignment.middle,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 5.0, top: 2.0, bottom: 2.0),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(5.0),
|
||||||
|
color: Colors.orange,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
text.trim(),
|
||||||
|
//style: textStyle?.copyWith(color: Colors.orange),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 5.0,
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 15.0,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: controller.text
|
||||||
|
.replaceRange(start, start + text.length, ""),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: start)));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (c) {
|
||||||
|
TextEditingController textEditingController =
|
||||||
|
TextEditingController()..text = text.trim();
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: textEditingController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
suffixIcon: FlatButton(
|
||||||
|
child: Text("OK"),
|
||||||
|
onPressed: () {
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: controller.text.replaceRange(
|
||||||
|
start,
|
||||||
|
start + text.length,
|
||||||
|
textEditingController.text + " "),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(
|
||||||
|
offset: start +
|
||||||
|
(textEditingController.text + " ")
|
||||||
|
.length)));
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
deleteAll: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## NoSystemKeyboard
|
||||||
|
|
||||||
|
support to prevent system keyboard show without any code intrusion for [ExtendedTextField] or [TextField].
|
||||||
|
|
||||||
|
### TextInputBindingMixin
|
||||||
|
|
||||||
|
we prevent system keyboard show by stop Flutter Framework send `TextInput.show` message to Flutter Engine.
|
||||||
|
|
||||||
|
you can use [TextInputBinding] directly.
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
void main() {
|
||||||
|
TextInputBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you have other `binding` you can do as following.
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
YourBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or you need to override `ignoreTextInputShow`, you can do as following.
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
class YourBinding extends TextInputBinding {
|
||||||
|
@override
|
||||||
|
// ignore: unnecessary_overrides
|
||||||
|
bool ignoreTextInputShow() {
|
||||||
|
// you can override it base on your case
|
||||||
|
// if NoKeyboardFocusNode is not enough
|
||||||
|
return super.ignoreTextInputShow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
YourBinding();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TextInputFocusNode
|
||||||
|
|
||||||
|
you should pass the [TextInputFocusNode] into [ExtendedTextField] or [TextField].
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
final TextInputFocusNode _focusNode = TextInputFocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ExtendedTextField(
|
||||||
|
// request keyboard if need
|
||||||
|
focusNode: _focusNode..debugLabel = 'ExtendedTextField',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
// request keyboard if need
|
||||||
|
focusNode: _focusNode..debugLabel = 'CustomTextField',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
we prevent system keyboard show base on current focus is [TextInputFocusNode] and `ignoreSystemKeyboardShow` is true。
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
final FocusNode? focus = FocusManager.instance.primaryFocus;
|
||||||
|
if (focus != null &&
|
||||||
|
focus is TextInputFocusNode &&
|
||||||
|
focus.ignoreSystemKeyboardShow) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
### CustomKeyboard
|
||||||
|
|
||||||
|
show/hide your custom keyboard on [TextInputFocusNode] focus is changed.
|
||||||
|
|
||||||
|
if your custom keyboard can be close without unFocus, you need also handle
|
||||||
|
show custom keyboard when [ExtendedTextField] or [TextField] `onTap`.
|
||||||
|
|
||||||
|
``` dart
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode.addListener(_handleFocusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextFiledTap() {
|
||||||
|
if (_bottomSheetController == null) {
|
||||||
|
_handleFocusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleFocusChanged() {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
// just demo, you can define your custom keyboard as you want
|
||||||
|
_bottomSheetController = showBottomSheet<void>(
|
||||||
|
context: FocusManager.instance.primaryFocus!.context!,
|
||||||
|
// set false, if don't want to drag to close custom keyboard
|
||||||
|
enableDrag: true,
|
||||||
|
builder: (BuildContext b) {
|
||||||
|
// your custom keyboard
|
||||||
|
return Container();
|
||||||
|
});
|
||||||
|
// maybe drag close
|
||||||
|
_bottomSheetController?.closed.whenComplete(() {
|
||||||
|
_bottomSheetController = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_bottomSheetController?.close();
|
||||||
|
_bottomSheetController = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_handleFocusChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
see [Full Demo](https://github.com/fluttercandies/extended_text_field/tree/master/example/lib/pages/simple/no_keyboard.dart)
|
||||||
@ -0,0 +1,203 @@
|
|||||||
|
# Specify analysis options.
|
||||||
|
#
|
||||||
|
# Until there are meta linter rules, each desired lint must be explicitly enabled.
|
||||||
|
# See: https://github.com/dart-lang/linter/issues/288
|
||||||
|
#
|
||||||
|
# For a list of lints, see: http://dart-lang.github.io/linter/lints/
|
||||||
|
# See the configuration guide for more
|
||||||
|
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
|
||||||
|
#
|
||||||
|
# There are other similar analysis options files in the flutter repos,
|
||||||
|
# which should be kept in sync with this file:
|
||||||
|
#
|
||||||
|
# - analysis_options.yaml (this file)
|
||||||
|
# - packages/flutter/lib/analysis_options_user.yaml
|
||||||
|
# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
|
||||||
|
# - https://github.com/flutter/engine/blob/master/analysis_options.yaml
|
||||||
|
#
|
||||||
|
# This file contains the analysis options used by Flutter tools, such as IntelliJ,
|
||||||
|
# Android Studio, and the `flutter analyze` command.
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
# treat missing required parameters as a warning (not a hint)
|
||||||
|
missing_required_param: warning
|
||||||
|
# treat missing returns as a warning (not a hint)
|
||||||
|
missing_return: warning
|
||||||
|
# allow having TODOs in the code
|
||||||
|
todo: ignore
|
||||||
|
# Ignore analyzer hints for updating pubspecs when using Future or
|
||||||
|
# Stream and not importing dart:async
|
||||||
|
# Please see https://github.com/flutter/flutter/pull/24528 for details.
|
||||||
|
# sdk_version_async_exported_from_core: ignore
|
||||||
|
# exclude:
|
||||||
|
# - "bin/cache/**"
|
||||||
|
# # the following two are relative to the stocks example and the flutter package respectively
|
||||||
|
# # see https://github.com/dart-lang/sdk/issues/28463
|
||||||
|
# - "lib/i18n/messages_*.dart"
|
||||||
|
# - "lib/src/http/**"
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
# these rules are documented on and in the same order as
|
||||||
|
# the Dart Lint rules page to make maintenance easier
|
||||||
|
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
||||||
|
- always_declare_return_types
|
||||||
|
- always_put_control_body_on_new_line
|
||||||
|
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
|
||||||
|
# - always_require_non_null_named_parameters
|
||||||
|
- always_specify_types
|
||||||
|
- annotate_overrides
|
||||||
|
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
|
||||||
|
# - avoid_as # required for implicit-casts: true
|
||||||
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
|
# - avoid_catches_without_on_clauses # we do this commonly
|
||||||
|
# - avoid_catching_errors # we do this commonly
|
||||||
|
- avoid_classes_with_only_static_members
|
||||||
|
# - avoid_double_and_int_checks # only useful when targeting JS runtime
|
||||||
|
- avoid_empty_else
|
||||||
|
# - avoid_equals_and_hash_code_on_mutable_classes # not yet tested
|
||||||
|
- avoid_field_initializers_in_const_classes
|
||||||
|
- avoid_function_literals_in_foreach_calls
|
||||||
|
# - avoid_implementing_value_types # not yet tested
|
||||||
|
- avoid_init_to_null
|
||||||
|
# - avoid_js_rounded_ints # only useful when targeting JS runtime
|
||||||
|
- avoid_null_checks_in_equality_operators
|
||||||
|
# - avoid_positional_boolean_parameters # not yet tested
|
||||||
|
# - avoid_print # not yet tested
|
||||||
|
# - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
|
||||||
|
# - avoid_redundant_argument_values # not yet tested
|
||||||
|
- avoid_relative_lib_imports
|
||||||
|
- avoid_renaming_method_parameters
|
||||||
|
- avoid_return_types_on_setters
|
||||||
|
# - avoid_returning_null # there are plenty of valid reasons to return null
|
||||||
|
# - avoid_returning_null_for_future # not yet tested
|
||||||
|
- avoid_returning_null_for_void
|
||||||
|
# - avoid_returning_this # there are plenty of valid reasons to return this
|
||||||
|
# - avoid_setters_without_getters # not yet tested
|
||||||
|
# - avoid_shadowing_type_parameters # not yet tested
|
||||||
|
- avoid_single_cascade_in_expression_statements
|
||||||
|
- avoid_slow_async_io
|
||||||
|
- avoid_types_as_parameter_names
|
||||||
|
# - avoid_types_on_closure_parameters # conflicts with always_specify_types
|
||||||
|
# - avoid_unnecessary_containers # not yet tested
|
||||||
|
- avoid_unused_constructor_parameters
|
||||||
|
- avoid_void_async
|
||||||
|
# - avoid_web_libraries_in_flutter # not yet tested
|
||||||
|
- await_only_futures
|
||||||
|
- camel_case_extensions
|
||||||
|
- camel_case_types
|
||||||
|
- cancel_subscriptions
|
||||||
|
# - cascade_invocations # not yet tested
|
||||||
|
# - close_sinks # not reliable enough
|
||||||
|
# - comment_references # blocked on https://github.com/flutter/flutter/issues/20765
|
||||||
|
# - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
|
||||||
|
- control_flow_in_finally
|
||||||
|
# - curly_braces_in_flow_control_structures # not yet tested
|
||||||
|
# - diagnostic_describe_all_properties # not yet tested
|
||||||
|
- directives_ordering
|
||||||
|
- empty_catches
|
||||||
|
- empty_constructor_bodies
|
||||||
|
- empty_statements
|
||||||
|
# - file_names # not yet tested
|
||||||
|
- flutter_style_todos
|
||||||
|
- hash_and_equals
|
||||||
|
- implementation_imports
|
||||||
|
# - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
|
||||||
|
# - iterable_contains_unrelated_type
|
||||||
|
# - join_return_with_assignment # not yet tested
|
||||||
|
- library_names
|
||||||
|
- library_prefixes
|
||||||
|
# - lines_longer_than_80_chars # not yet tested
|
||||||
|
# - list_remove_unrelated_type
|
||||||
|
# - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
|
||||||
|
# - missing_whitespace_between_adjacent_strings # not yet tested
|
||||||
|
- no_adjacent_strings_in_list
|
||||||
|
- no_duplicate_case_values
|
||||||
|
# - no_logic_in_create_state # not yet tested
|
||||||
|
# - no_runtimeType_toString # not yet tested
|
||||||
|
- non_constant_identifier_names
|
||||||
|
# - null_closures # not yet tested
|
||||||
|
# - omit_local_variable_types # opposite of always_specify_types
|
||||||
|
# - one_member_abstracts # too many false positives
|
||||||
|
# - only_throw_errors # https://github.com/flutter/flutter/issues/5792
|
||||||
|
- overridden_fields
|
||||||
|
- package_api_docs
|
||||||
|
- package_names
|
||||||
|
- package_prefixed_library_names
|
||||||
|
# - parameter_assignments # we do this commonly
|
||||||
|
- prefer_adjacent_string_concatenation
|
||||||
|
- prefer_asserts_in_initializer_lists
|
||||||
|
# - prefer_asserts_with_message # not yet tested
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_conditional_assignment
|
||||||
|
- prefer_const_constructors
|
||||||
|
- prefer_const_constructors_in_immutables
|
||||||
|
- prefer_const_declarations
|
||||||
|
- prefer_const_literals_to_create_immutables
|
||||||
|
# - prefer_constructors_over_static_methods # not yet tested
|
||||||
|
- prefer_contains
|
||||||
|
# - prefer_double_quotes # opposite of prefer_single_quotes
|
||||||
|
# - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
|
||||||
|
- prefer_final_fields
|
||||||
|
- prefer_final_in_for_each
|
||||||
|
- prefer_final_locals
|
||||||
|
- prefer_for_elements_to_map_fromIterable
|
||||||
|
- prefer_foreach
|
||||||
|
# - prefer_function_declarations_over_variables # not yet tested
|
||||||
|
- prefer_generic_function_type_aliases
|
||||||
|
- prefer_if_elements_to_conditional_expressions
|
||||||
|
- prefer_if_null_operators
|
||||||
|
- prefer_initializing_formals
|
||||||
|
- prefer_inlined_adds
|
||||||
|
# - prefer_int_literals # not yet tested
|
||||||
|
# - prefer_interpolation_to_compose_strings # not yet tested
|
||||||
|
- prefer_is_empty
|
||||||
|
- prefer_is_not_empty
|
||||||
|
- prefer_is_not_operator
|
||||||
|
- prefer_iterable_whereType
|
||||||
|
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
||||||
|
# - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932
|
||||||
|
# - prefer_relative_imports # not yet tested
|
||||||
|
- prefer_single_quotes
|
||||||
|
- prefer_spread_collections
|
||||||
|
- prefer_typing_uninitialized_variables
|
||||||
|
- prefer_void_to_null
|
||||||
|
# - provide_deprecation_message # not yet tested
|
||||||
|
# - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml
|
||||||
|
- recursive_getters
|
||||||
|
- slash_for_doc_comments
|
||||||
|
# - sort_child_properties_last # not yet tested
|
||||||
|
- sort_constructors_first
|
||||||
|
# - sort_pub_dependencies
|
||||||
|
- sort_unnamed_constructors_first
|
||||||
|
- test_types_in_equals
|
||||||
|
- throw_in_finally
|
||||||
|
# - type_annotate_public_apis # subset of always_specify_types
|
||||||
|
- type_init_formals
|
||||||
|
# - unawaited_futures # too many false positives
|
||||||
|
# - unnecessary_await_in_return # not yet tested
|
||||||
|
- unnecessary_brace_in_string_interps
|
||||||
|
- unnecessary_const
|
||||||
|
# - unnecessary_final # conflicts with prefer_final_locals
|
||||||
|
- unnecessary_getters_setters
|
||||||
|
# - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
|
||||||
|
- unnecessary_new
|
||||||
|
- unnecessary_null_aware_assignments
|
||||||
|
- unnecessary_null_in_if_null_operators
|
||||||
|
- unnecessary_overrides
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_statements
|
||||||
|
- unnecessary_string_interpolations
|
||||||
|
- unnecessary_this
|
||||||
|
- unrelated_type_equality_checks
|
||||||
|
# - unsafe_html # not yet tested
|
||||||
|
- use_full_hex_values_for_flutter_colors
|
||||||
|
# - use_function_type_syntax_for_parameters # not yet tested
|
||||||
|
# - use_key_in_widget_constructors # not yet tested
|
||||||
|
- use_rethrow_when_possible
|
||||||
|
# - use_setters_to_change_properties # not yet tested
|
||||||
|
# - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
|
||||||
|
# - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
|
||||||
|
- valid_regexps
|
||||||
|
- void_checks
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# example
|
||||||
|
|
||||||
|
A new Flutter application.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter, view our
|
||||||
|
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
@ -0,0 +1,203 @@
|
|||||||
|
# Specify analysis options.
|
||||||
|
#
|
||||||
|
# Until there are meta linter rules, each desired lint must be explicitly enabled.
|
||||||
|
# See: https://github.com/dart-lang/linter/issues/288
|
||||||
|
#
|
||||||
|
# For a list of lints, see: http://dart-lang.github.io/linter/lints/
|
||||||
|
# See the configuration guide for more
|
||||||
|
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
|
||||||
|
#
|
||||||
|
# There are other similar analysis options files in the flutter repos,
|
||||||
|
# which should be kept in sync with this file:
|
||||||
|
#
|
||||||
|
# - analysis_options.yaml (this file)
|
||||||
|
# - packages/flutter/lib/analysis_options_user.yaml
|
||||||
|
# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
|
||||||
|
# - https://github.com/flutter/engine/blob/master/analysis_options.yaml
|
||||||
|
#
|
||||||
|
# This file contains the analysis options used by Flutter tools, such as IntelliJ,
|
||||||
|
# Android Studio, and the `flutter analyze` command.
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
# treat missing required parameters as a warning (not a hint)
|
||||||
|
missing_required_param: warning
|
||||||
|
# treat missing returns as a warning (not a hint)
|
||||||
|
missing_return: warning
|
||||||
|
# allow having TODOs in the code
|
||||||
|
todo: ignore
|
||||||
|
# Ignore analyzer hints for updating pubspecs when using Future or
|
||||||
|
# Stream and not importing dart:async
|
||||||
|
# Please see https://github.com/flutter/flutter/pull/24528 for details.
|
||||||
|
# sdk_version_async_exported_from_core: ignore
|
||||||
|
# exclude:
|
||||||
|
# - "bin/cache/**"
|
||||||
|
# # the following two are relative to the stocks example and the flutter package respectively
|
||||||
|
# # see https://github.com/dart-lang/sdk/issues/28463
|
||||||
|
# - "lib/i18n/messages_*.dart"
|
||||||
|
# - "lib/src/http/**"
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
# these rules are documented on and in the same order as
|
||||||
|
# the Dart Lint rules page to make maintenance easier
|
||||||
|
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
||||||
|
- always_declare_return_types
|
||||||
|
- always_put_control_body_on_new_line
|
||||||
|
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
|
||||||
|
# - always_require_non_null_named_parameters
|
||||||
|
- always_specify_types
|
||||||
|
- annotate_overrides
|
||||||
|
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
|
||||||
|
# - avoid_as # required for implicit-casts: true
|
||||||
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
|
# - avoid_catches_without_on_clauses # we do this commonly
|
||||||
|
# - avoid_catching_errors # we do this commonly
|
||||||
|
- avoid_classes_with_only_static_members
|
||||||
|
# - avoid_double_and_int_checks # only useful when targeting JS runtime
|
||||||
|
- avoid_empty_else
|
||||||
|
# - avoid_equals_and_hash_code_on_mutable_classes # not yet tested
|
||||||
|
- avoid_field_initializers_in_const_classes
|
||||||
|
- avoid_function_literals_in_foreach_calls
|
||||||
|
# - avoid_implementing_value_types # not yet tested
|
||||||
|
- avoid_init_to_null
|
||||||
|
# - avoid_js_rounded_ints # only useful when targeting JS runtime
|
||||||
|
- avoid_null_checks_in_equality_operators
|
||||||
|
# - avoid_positional_boolean_parameters # not yet tested
|
||||||
|
# - avoid_print # not yet tested
|
||||||
|
# - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
|
||||||
|
# - avoid_redundant_argument_values # not yet tested
|
||||||
|
- avoid_relative_lib_imports
|
||||||
|
- avoid_renaming_method_parameters
|
||||||
|
- avoid_return_types_on_setters
|
||||||
|
# - avoid_returning_null # there are plenty of valid reasons to return null
|
||||||
|
# - avoid_returning_null_for_future # not yet tested
|
||||||
|
- avoid_returning_null_for_void
|
||||||
|
# - avoid_returning_this # there are plenty of valid reasons to return this
|
||||||
|
# - avoid_setters_without_getters # not yet tested
|
||||||
|
# - avoid_shadowing_type_parameters # not yet tested
|
||||||
|
- avoid_single_cascade_in_expression_statements
|
||||||
|
- avoid_slow_async_io
|
||||||
|
- avoid_types_as_parameter_names
|
||||||
|
# - avoid_types_on_closure_parameters # conflicts with always_specify_types
|
||||||
|
# - avoid_unnecessary_containers # not yet tested
|
||||||
|
- avoid_unused_constructor_parameters
|
||||||
|
- avoid_void_async
|
||||||
|
# - avoid_web_libraries_in_flutter # not yet tested
|
||||||
|
- await_only_futures
|
||||||
|
- camel_case_extensions
|
||||||
|
- camel_case_types
|
||||||
|
- cancel_subscriptions
|
||||||
|
# - cascade_invocations # not yet tested
|
||||||
|
# - close_sinks # not reliable enough
|
||||||
|
# - comment_references # blocked on https://github.com/flutter/flutter/issues/20765
|
||||||
|
# - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
|
||||||
|
- control_flow_in_finally
|
||||||
|
# - curly_braces_in_flow_control_structures # not yet tested
|
||||||
|
# - diagnostic_describe_all_properties # not yet tested
|
||||||
|
- directives_ordering
|
||||||
|
- empty_catches
|
||||||
|
- empty_constructor_bodies
|
||||||
|
- empty_statements
|
||||||
|
# - file_names # not yet tested
|
||||||
|
- flutter_style_todos
|
||||||
|
- hash_and_equals
|
||||||
|
- implementation_imports
|
||||||
|
# - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
|
||||||
|
# - iterable_contains_unrelated_type
|
||||||
|
# - join_return_with_assignment # not yet tested
|
||||||
|
- library_names
|
||||||
|
- library_prefixes
|
||||||
|
# - lines_longer_than_80_chars # not yet tested
|
||||||
|
# - list_remove_unrelated_type
|
||||||
|
# - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
|
||||||
|
# - missing_whitespace_between_adjacent_strings # not yet tested
|
||||||
|
- no_adjacent_strings_in_list
|
||||||
|
- no_duplicate_case_values
|
||||||
|
# - no_logic_in_create_state # not yet tested
|
||||||
|
# - no_runtimeType_toString # not yet tested
|
||||||
|
- non_constant_identifier_names
|
||||||
|
# - null_closures # not yet tested
|
||||||
|
# - omit_local_variable_types # opposite of always_specify_types
|
||||||
|
# - one_member_abstracts # too many false positives
|
||||||
|
# - only_throw_errors # https://github.com/flutter/flutter/issues/5792
|
||||||
|
- overridden_fields
|
||||||
|
- package_api_docs
|
||||||
|
- package_names
|
||||||
|
- package_prefixed_library_names
|
||||||
|
# - parameter_assignments # we do this commonly
|
||||||
|
- prefer_adjacent_string_concatenation
|
||||||
|
- prefer_asserts_in_initializer_lists
|
||||||
|
# - prefer_asserts_with_message # not yet tested
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_conditional_assignment
|
||||||
|
- prefer_const_constructors
|
||||||
|
- prefer_const_constructors_in_immutables
|
||||||
|
- prefer_const_declarations
|
||||||
|
- prefer_const_literals_to_create_immutables
|
||||||
|
# - prefer_constructors_over_static_methods # not yet tested
|
||||||
|
- prefer_contains
|
||||||
|
# - prefer_double_quotes # opposite of prefer_single_quotes
|
||||||
|
# - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
|
||||||
|
- prefer_final_fields
|
||||||
|
- prefer_final_in_for_each
|
||||||
|
- prefer_final_locals
|
||||||
|
- prefer_for_elements_to_map_fromIterable
|
||||||
|
- prefer_foreach
|
||||||
|
# - prefer_function_declarations_over_variables # not yet tested
|
||||||
|
- prefer_generic_function_type_aliases
|
||||||
|
- prefer_if_elements_to_conditional_expressions
|
||||||
|
- prefer_if_null_operators
|
||||||
|
- prefer_initializing_formals
|
||||||
|
- prefer_inlined_adds
|
||||||
|
# - prefer_int_literals # not yet tested
|
||||||
|
# - prefer_interpolation_to_compose_strings # not yet tested
|
||||||
|
- prefer_is_empty
|
||||||
|
- prefer_is_not_empty
|
||||||
|
- prefer_is_not_operator
|
||||||
|
- prefer_iterable_whereType
|
||||||
|
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
||||||
|
# - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932
|
||||||
|
# - prefer_relative_imports # not yet tested
|
||||||
|
- prefer_single_quotes
|
||||||
|
- prefer_spread_collections
|
||||||
|
- prefer_typing_uninitialized_variables
|
||||||
|
- prefer_void_to_null
|
||||||
|
# - provide_deprecation_message # not yet tested
|
||||||
|
# - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml
|
||||||
|
- recursive_getters
|
||||||
|
- slash_for_doc_comments
|
||||||
|
#- sort_child_properties_last # not yet tested
|
||||||
|
- sort_constructors_first
|
||||||
|
#- sort_pub_dependencies
|
||||||
|
- sort_unnamed_constructors_first
|
||||||
|
- test_types_in_equals
|
||||||
|
- throw_in_finally
|
||||||
|
# - type_annotate_public_apis # subset of always_specify_types
|
||||||
|
- type_init_formals
|
||||||
|
# - unawaited_futures # too many false positives
|
||||||
|
# - unnecessary_await_in_return # not yet tested
|
||||||
|
- unnecessary_brace_in_string_interps
|
||||||
|
- unnecessary_const
|
||||||
|
# - unnecessary_final # conflicts with prefer_final_locals
|
||||||
|
- unnecessary_getters_setters
|
||||||
|
# - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
|
||||||
|
- unnecessary_new
|
||||||
|
- unnecessary_null_aware_assignments
|
||||||
|
- unnecessary_null_in_if_null_operators
|
||||||
|
- unnecessary_overrides
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_statements
|
||||||
|
- unnecessary_string_interpolations
|
||||||
|
- unnecessary_this
|
||||||
|
- unrelated_type_equality_checks
|
||||||
|
# - unsafe_html # not yet tested
|
||||||
|
- use_full_hex_values_for_flutter_colors
|
||||||
|
# - use_function_type_syntax_for_parameters # not yet tested
|
||||||
|
# - use_key_in_widget_constructors # not yet tested
|
||||||
|
- use_rethrow_when_possible
|
||||||
|
# - use_setters_to_change_properties # not yet tested
|
||||||
|
# - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
|
||||||
|
# - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
|
||||||
|
- valid_regexps
|
||||||
|
- void_checks
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.example.example"
|
||||||
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.example.example"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
minSdkVersion flutter.minSdkVersion
|
||||||
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="example"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.example.example
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity() {
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.7.10'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4G
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
|
id "com.android.application" version "7.3.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.6 KiB |