feat: update home room and chat experience
@ -138,7 +138,7 @@
|
||||
android:name=".VoiceRoomForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback|microphone"
|
||||
android:stopWithTask="false" />
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<meta-data
|
||||
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.os.Build
|
||||
import android.os.IBinder
|
||||
import com.tencent.trtc.TRTCCloud
|
||||
|
||||
class VoiceRoomForegroundService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
explicitStopRequested = false
|
||||
ensureNotificationChannel()
|
||||
}
|
||||
|
||||
@ -28,6 +30,32 @@ class VoiceRoomForegroundService : Service() {
|
||||
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) {
|
||||
val notification = buildNotification(roomName)
|
||||
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 DEFAULT_ROOM_NAME = "Voice room is running"
|
||||
|
||||
@Volatile
|
||||
private var explicitStopRequested = false
|
||||
|
||||
fun start(context: Context, roomName: String?) {
|
||||
val intent = Intent(context, VoiceRoomForegroundService::class.java)
|
||||
.putExtra(EXTRA_ROOM_NAME, roomName)
|
||||
@ -127,6 +158,7 @@ class VoiceRoomForegroundService : Service() {
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
explicitStopRequested = true
|
||||
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,3 +7,7 @@ android.enableJetifier=true
|
||||
systemProp.https.protocols=TLSv1.2
|
||||
systemProp.jdk.tls.client.protocols=TLSv1.2
|
||||
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": "المظروف الأحمر لم يُستلم بعد",
|
||||
"redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات",
|
||||
"followed": "تمت المتابعة",
|
||||
"joined": "انضممت",
|
||||
"newMessage": "رسالة جديدة",
|
||||
"keep": "الاحتفاظ",
|
||||
"open": "فتح",
|
||||
@ -233,7 +234,8 @@
|
||||
"alreadyAnTourist": "أنت بالفعل سائح",
|
||||
"deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟",
|
||||
"account": "الحساب",
|
||||
"youHaventFollowed": "أنت لم تتابع أي غرفة",
|
||||
"youHaventFollowed": "أنت لم تتابع أي غرفة",
|
||||
"youHaventJoinedAnyRoomsYet": "لم أنضم إلى أي غرف بعد.",
|
||||
"messageHasBeenRecalled": "تم استرجاع هذه الرسالة",
|
||||
"propMessagePrompt": "موجه رسالة دعائية",
|
||||
"deleteFromMyDevice": "احذف من جهازي",
|
||||
@ -559,7 +561,8 @@
|
||||
"coinsReceived": "العملات المستلمة",
|
||||
"hotRooms": "الغرف الرائجة",
|
||||
"viewMore": "عرض المزيد",
|
||||
"noData": "لا توجد بيانات",
|
||||
"noData": "لا توجد بيانات",
|
||||
"noRoomRecordsRecently": "لا توجد سجلات غرف حديثة.",
|
||||
"recommend": "موصى به",
|
||||
"follow": "متابعة",
|
||||
"monthly": "شهريًا",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
|
||||
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
|
||||
"launchRocket": "إطلاق الصاروخ",
|
||||
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!"
|
||||
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!",
|
||||
"leaderboardWealth": "الثروة",
|
||||
"leaderboardCharm": "الجاذبية",
|
||||
"leaderboardRoom": "الغرفة"
|
||||
}
|
||||
|
||||
@ -258,6 +258,7 @@
|
||||
"youDontHaveAnyCouponsYet": "আপনার এখনও কোনো কুপন নেই।",
|
||||
"recall": "প্রত্যাহার করুন",
|
||||
"youHaventFollowed": "আপনি কোনো রুম ফলো করেন না",
|
||||
"youHaventJoinedAnyRoomsYet": "আমি এখনও কোনো রুমে যোগ দিইনি।",
|
||||
"deleteFromMyDevice": "আমার ডিভাইস থেকে মুছে ফেলুন",
|
||||
"deleteOnAllDevices": "সমস্ত ডিভাইসে মুছে ফেলুন",
|
||||
"messageHasBeenRecalled": "এই বার্তাটি প্রত্যাহার করা হয়েছে",
|
||||
@ -387,6 +388,7 @@
|
||||
"hotRooms": "হট রুম",
|
||||
"viewMore": "আরও দেখুন",
|
||||
"noData": "ডেটা নেই",
|
||||
"noRoomRecordsRecently": "সম্প্রতি কোনো রুমের রেকর্ড নেই।",
|
||||
"users": "ব্যবহারকারী",
|
||||
"rooms": "রুম",
|
||||
"coins": "কয়েন",
|
||||
@ -646,6 +648,7 @@
|
||||
"purchaseIsSuccessful": "ক্রয় সফল",
|
||||
"buy": "কিনুন",
|
||||
"followed": "ফলো করা হয়েছে",
|
||||
"joined": "যোগদান করা",
|
||||
"follow2": "ফলো:{1}",
|
||||
"fans2": "ফ্যান:{1}",
|
||||
"vistors2": "ভিজিটর:{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
|
||||
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
|
||||
"launchRocket": "রকেট চালু করুন",
|
||||
"roomRocketFuelFullNotice": "রকেটের জ্বালানি পূর্ণ! {1} সেকেন্ড পরে উৎক্ষেপণ!"
|
||||
"roomRocketFuelFullNotice": "রকেটের জ্বালানি পূর্ণ! {1} সেকেন্ড পরে উৎক্ষেপণ!",
|
||||
"leaderboardWealth": "সম্পদ",
|
||||
"leaderboardCharm": "আকর্ষণ",
|
||||
"leaderboardRoom": "রুম"
|
||||
}
|
||||
|
||||
@ -228,6 +228,7 @@
|
||||
"youDontHaveAnyCouponsYet": "You don't have any coupons yet.",
|
||||
"recall": "Recall",
|
||||
"youHaventFollowed": "You haven't followed any room",
|
||||
"youHaventJoinedAnyRoomsYet": "I haven't joined any rooms yet.",
|
||||
"deleteFromMyDevice": "Delete from my device",
|
||||
"deleteOnAllDevices": "Delete on all devices",
|
||||
"messageHasBeenRecalled": "This message has been recalled",
|
||||
@ -357,6 +358,7 @@
|
||||
"hotRooms": "Hot Rooms",
|
||||
"viewMore": "View more",
|
||||
"noData": "No data",
|
||||
"noRoomRecordsRecently": "No room records recently.",
|
||||
"users": "Users",
|
||||
"rooms": "Rooms",
|
||||
"coins": "coins",
|
||||
@ -646,6 +648,7 @@
|
||||
"purchaseIsSuccessful": "Purchase is successful",
|
||||
"buy": "Buy",
|
||||
"followed": "Followed",
|
||||
"joined": "Joined",
|
||||
"follow2": "Follow:{1}",
|
||||
"fans2": "Fans:{1}",
|
||||
"vistors2": "Visitors:{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "No reward",
|
||||
"roomRocketRecordEmptyText": "No reward records",
|
||||
"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.",
|
||||
"recall": "Rappel",
|
||||
"youHaventFollowed": "Vous n'avez suivi aucune pièce",
|
||||
"youHaventJoinedAnyRoomsYet": "Je n’ai encore rejoint aucune salle.",
|
||||
"deleteFromMyDevice": "Supprimer de mon appareil",
|
||||
"deleteOnAllDevices": "Supprimer sur tous les appareils",
|
||||
"messageHasBeenRecalled": "Ce message a été rappelé",
|
||||
@ -357,6 +358,7 @@
|
||||
"hotRooms": "Chambres chaudes",
|
||||
"viewMore": "Voir plus",
|
||||
"noData": "Aucune donnée",
|
||||
"noRoomRecordsRecently": "Aucun historique de salle récent.",
|
||||
"users": "Utilisateurs",
|
||||
"rooms": "Chambres",
|
||||
"coins": "pièces de monnaie",
|
||||
@ -646,6 +648,7 @@
|
||||
"purchaseIsSuccessful": "L'achat est réussi",
|
||||
"buy": "Acheter",
|
||||
"followed": "Suivi",
|
||||
"joined": "Rejointes",
|
||||
"follow2": "Suivre : {1}",
|
||||
"fans2": "Ventilateurs : {1}",
|
||||
"vistors2": "Visiteurs :{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "Aucune récompense gagnée. Plus de chance la prochaine fois.",
|
||||
"roomRocketRecordEmptyText": "Aucun enregistrement de récompense",
|
||||
"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.",
|
||||
"recall": "Ricorda",
|
||||
"youHaventFollowed": "Non hai seguito nessuna stanza",
|
||||
"youHaventJoinedAnyRoomsYet": "Non ho ancora partecipato a nessuna stanza.",
|
||||
"deleteFromMyDevice": "Elimina dal mio dispositivo",
|
||||
"deleteOnAllDevices": "Elimina su tutti i dispositivi",
|
||||
"messageHasBeenRecalled": "Questo messaggio è stato richiamato",
|
||||
@ -357,6 +358,7 @@
|
||||
"hotRooms": "Camere calde",
|
||||
"viewMore": "Visualizza altro",
|
||||
"noData": "Nessun dato",
|
||||
"noRoomRecordsRecently": "Nessun record recente delle stanze.",
|
||||
"users": "Utenti",
|
||||
"rooms": "Camere",
|
||||
"coins": "monete",
|
||||
@ -646,6 +648,7 @@
|
||||
"purchaseIsSuccessful": "L'acquisto è andato a buon fine",
|
||||
"buy": "Acquista",
|
||||
"followed": "Seguito",
|
||||
"joined": "Partecipate",
|
||||
"follow2": "Segui:{1}",
|
||||
"fans2": "Ventole:{1}",
|
||||
"vistors2": "Visitatori:{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "Nessun premio guadagnato. Più fortuna la prossima volta.",
|
||||
"roomRocketRecordEmptyText": "Nessun record di ricompensa",
|
||||
"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.",
|
||||
"recall": "Lembre-se",
|
||||
"youHaventFollowed": "Você não seguiu nenhuma sala",
|
||||
"youHaventJoinedAnyRoomsYet": "Ainda não entrei em nenhuma sala.",
|
||||
"deleteFromMyDevice": "Excluir do meu dispositivo",
|
||||
"deleteOnAllDevices": "Excluir em todos os dispositivos",
|
||||
"messageHasBeenRecalled": "Esta mensagem foi recuperada",
|
||||
@ -357,6 +358,7 @@
|
||||
"hotRooms": "Salas Quentes",
|
||||
"viewMore": "Ver mais",
|
||||
"noData": "Sem dados",
|
||||
"noRoomRecordsRecently": "Nenhum registro recente de salas.",
|
||||
"users": "Usuários",
|
||||
"rooms": "Quartos",
|
||||
"coins": "moedas",
|
||||
@ -646,6 +648,7 @@
|
||||
"purchaseIsSuccessful": "A compra foi bem-sucedida",
|
||||
"buy": "Comprar",
|
||||
"followed": "Seguido",
|
||||
"joined": "Participadas",
|
||||
"follow2": "Siga: {1}",
|
||||
"fans2": "Fãs: {1}",
|
||||
"vistors2": "Visitantes:{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "Nenhuma recompensa ganha. Melhor sorte da próxima vez.",
|
||||
"roomRocketRecordEmptyText": "Nenhum registro de recompensa",
|
||||
"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?",
|
||||
"youDontHaveAnyCouponsYet": "Henüz hiç kuponunuz yok.",
|
||||
"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",
|
||||
"deleteOnAllDevices": "Tüm Cihazlarda Sil",
|
||||
"messageHasBeenRecalled": "Bu mesaj geri çekildi",
|
||||
@ -355,7 +356,8 @@
|
||||
"history": "Tarihçe",
|
||||
"hotRooms": "Popüler Odalar",
|
||||
"viewMore": "Daha Fazlasını Gör",
|
||||
"noData": "Veri yok",
|
||||
"noData": "Veri yok",
|
||||
"noRoomRecordsRecently": "Yakın zamanda oda kaydı yok.",
|
||||
"users": "Kullanıcılar",
|
||||
"rooms": "Odalar",
|
||||
"coins": "jetton",
|
||||
@ -614,7 +616,8 @@
|
||||
"mountains": "Araçlar",
|
||||
"purchaseIsSuccessful": "Satın alma başarılı",
|
||||
"buy": "Satın Al",
|
||||
"followed": "Takip Edildi",
|
||||
"followed": "Takip Edildi",
|
||||
"joined": "Katıldıklarım",
|
||||
"follow2": "Takip:{1}",
|
||||
"fans2": "Hayranlar:{1}",
|
||||
"vistors2": "Ziyaretçiler:{1}",
|
||||
@ -748,5 +751,8 @@
|
||||
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
|
||||
"roomRocketRecordEmptyText": "Ödül kaydı yok",
|
||||
"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
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
|
||||
@ -4,6 +4,7 @@ class SCRoomMsgType {
|
||||
static const String joinRoom = "JOIN_ROOM";
|
||||
static const String systemTips = "SYSTEM_TIPS";
|
||||
static const String cpSystemNotice = "CP_SYSTEM_NOTICE";
|
||||
static const String cpRelationBroadcast = "CP_RELATION_BROADCAST";
|
||||
|
||||
///表情包
|
||||
static const String emoticons = "EMOTICONS";
|
||||
|
||||
@ -1195,6 +1195,12 @@ class SCAppLocalizations {
|
||||
|
||||
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 giftCounter => translate('giftCounter');
|
||||
@ -1533,6 +1539,8 @@ class SCAppLocalizations {
|
||||
|
||||
String get followed => translate('followed');
|
||||
|
||||
String get joined => translate('joined');
|
||||
|
||||
String get following => translate('following');
|
||||
|
||||
String get history => translate('history');
|
||||
@ -1543,6 +1551,11 @@ class SCAppLocalizations {
|
||||
|
||||
String get noData => translate('noData');
|
||||
|
||||
String get noRoomRecordsRecently => translate('noRoomRecordsRecently');
|
||||
|
||||
String get youHaventJoinedAnyRoomsYet =>
|
||||
translate('youHaventJoinedAnyRoomsYet');
|
||||
|
||||
String get freePrice => translate('freePrice');
|
||||
|
||||
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_gift_vap_svga_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 'services/general/sc_app_general_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<ModalRoute<dynamic>> modalRouteObserver =
|
||||
RouteObserver<ModalRoute<dynamic>>();
|
||||
final NavigatorObserver floatingScreenRouteObserver =
|
||||
SCFloatingScreenRouteObserver();
|
||||
|
||||
class YumiApplication extends StatefulWidget {
|
||||
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/user_manager.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/services/general/sc_app_general_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 =
|
||||
null;
|
||||
_prefillGoogleRegistrationProfile();
|
||||
_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
|
||||
void dispose() {
|
||||
nicknameController.dispose();
|
||||
|
||||
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:provider/provider.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/ui_kit/theme/socialchat_theme.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';
|
||||
|
||||
@ -94,101 +94,29 @@ class _MessagePageState extends State<SCMessagePage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.isFromRoom
|
||||
? Image.asset(
|
||||
_strategy.getMessagePageIndexMaskIcon(),
|
||||
width: ScreenUtil().screenWidth,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(),
|
||||
DefaultTabController(
|
||||
length: 1,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
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(
|
||||
const Positioned.fill(child: SCHomeShellBackground()),
|
||||
if (widget.isFromRoom)
|
||||
Image.asset(
|
||||
_strategy.getMessagePageIndexMaskIcon(),
|
||||
width: ScreenUtil().screenWidth,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Consumer<RtmProvider>(
|
||||
builder: (_, provider, __) {
|
||||
return Container(
|
||||
margin: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"sc_images/index/sc_index_msg_content_bg.png",
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
_buildTopBar(),
|
||||
Expanded(
|
||||
child: Consumer<RtmProvider>(
|
||||
builder: (_, provider, __) {
|
||||
return Column(
|
||||
children: [
|
||||
if (!_isSearchingMessages) _headMenu(provider),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 0.w,
|
||||
left: 8.w,
|
||||
right: 8.w,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: MessageConversationListPage(
|
||||
false,
|
||||
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() {
|
||||
return Container(
|
||||
height: 36.w,
|
||||
@ -232,7 +205,7 @@ class _MessagePageState extends State<SCMessagePage> {
|
||||
focusNode: _messageSearchFocusNode,
|
||||
autofocus: true,
|
||||
cursorColor: SocialChatTheme.primaryLight,
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.sp),
|
||||
style: TextStyle(color: Colors.black, fontSize: 14.sp),
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
"${SCAppLocalizations.of(context)!.search} ID/${SCAppLocalizations.of(context)!.userName}",
|
||||
@ -275,72 +248,42 @@ class _MessagePageState extends State<SCMessagePage> {
|
||||
|
||||
Widget _headMenu(RtmProvider provider) {
|
||||
return Container(
|
||||
margin: EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 20.w,
|
||||
).copyWith(top: 10.w),
|
||||
child: Column(
|
||||
spacing: 15.w,
|
||||
margin: EdgeInsets.fromLTRB(10.w, 25.w, 10.w, 10.w),
|
||||
height: 114.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.68),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SCDebounceWidget(
|
||||
child: Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(height: 3.w),
|
||||
Image.asset(
|
||||
_strategy.getMessagePageActivityMessageIcon(),
|
||||
width: 45.w,
|
||||
height: 45.w,
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.activity,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildMessageShortcut(
|
||||
label: SCAppLocalizations.of(context)!.notifcation,
|
||||
icon: _strategy.getMessagePageNotificationMessageIcon(),
|
||||
unreadCount: provider.notifcationUnReadCount,
|
||||
onTap: () {
|
||||
provider.updateNotificationCount(0);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
SCChatRouter.notifcation,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildMessageShortcut(
|
||||
label: SCAppLocalizations.of(context)!.activity,
|
||||
icon: _strategy.getMessagePageActivityMessageIcon(),
|
||||
unreadCount: provider.activityUnReadCount,
|
||||
onTap: () {
|
||||
provider.updateActivityCount(0);
|
||||
SCNavigatorUtils.push(
|
||||
@ -350,45 +293,10 @@ class _MessagePageState extends State<SCMessagePage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
SCDebounceWidget(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(height: 3.w),
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildMessageShortcut(
|
||||
label: SCAppLocalizations.of(context)!.system,
|
||||
icon: _strategy.getMessagePageSystemMessageIcon(),
|
||||
unreadCount: provider.systemUnReadCount,
|
||||
onTap: () async {
|
||||
var conversation = provider.getPreferredSystemConversation();
|
||||
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: [
|
||||
SizedBox(height: 3.w),
|
||||
Image.asset(
|
||||
_strategy.getMessagePageNotificationMessageIcon(),
|
||||
width: 45.w,
|
||||
height: 45.w,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
Image.asset(icon, width: 52.w, height: 52.w),
|
||||
if (unreadCount > 0)
|
||||
PositionedDirectional(
|
||||
top: -2.w,
|
||||
end: -6.w,
|
||||
child: _unreadBadge(unreadCount),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
provider.updateNotificationCount(0);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
SCChatRouter.notifcation,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
SizedBox(height: 10.w),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
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(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
alignment: Alignment.center,
|
||||
height: 16.w,
|
||||
height: 18.w,
|
||||
constraints: BoxConstraints(minWidth: 18.w),
|
||||
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
|
||||
child: Text(
|
||||
"${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/routes/sc_fluro_navigator.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_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
@ -2623,6 +2624,17 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
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 =
|
||||
(message.isSelf ?? false)
|
||||
? _currentUserChatBubble()
|
||||
@ -2697,8 +2709,8 @@ class _MessageItem extends StatelessWidget {
|
||||
260.w,
|
||||
);
|
||||
final multilineMaxTextWidth = math.max(
|
||||
82.w,
|
||||
multilineMaxBubbleWidth - 76.w,
|
||||
60.w,
|
||||
multilineMaxBubbleWidth - 52.w,
|
||||
);
|
||||
final multilineTextPainter = TextPainter(
|
||||
text: TextSpan(text: content, style: textStyle),
|
||||
@ -2728,34 +2740,24 @@ class _MessageItem extends StatelessWidget {
|
||||
required Size textSize,
|
||||
required double maxBubbleWidth,
|
||||
}) {
|
||||
final leftPadding = 34.w;
|
||||
final rightPadding = 42.w;
|
||||
final topPadding = 16.w;
|
||||
final bottomPadding = 18.w;
|
||||
final leftPadding = 24.w;
|
||||
final rightPadding = 28.w;
|
||||
final topPadding = 10.w;
|
||||
final bottomPadding = 12.w;
|
||||
final bubbleWidth = math
|
||||
.max(126.w, textSize.width + leftPadding + rightPadding)
|
||||
.clamp(126.w, maxBubbleWidth);
|
||||
.max(92.w, textSize.width + leftPadding + rightPadding)
|
||||
.clamp(92.w, maxBubbleWidth);
|
||||
final textMaxWidth = math.max(
|
||||
64.w,
|
||||
40.w,
|
||||
bubbleWidth - leftPadding - rightPadding,
|
||||
);
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: content, style: textStyle),
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: textMaxWidth);
|
||||
final lineMetrics = textPainter.computeLineMetrics();
|
||||
final lineCount = math.max(1, lineMetrics.length);
|
||||
final lineHeight =
|
||||
lineMetrics.isEmpty
|
||||
? textPainter.preferredLineHeight
|
||||
: lineMetrics.fold<double>(
|
||||
0,
|
||||
(height, line) => math.max(height, line.height),
|
||||
);
|
||||
final lineSafetyPadding = lineHeight * math.max(0, lineCount - 1) * 0.55;
|
||||
final textBoxHeight = textPainter.size.height + lineSafetyPadding;
|
||||
final textBoxHeight = textPainter.size.height + 2.w;
|
||||
final bubbleHeight = math.max(
|
||||
76.w,
|
||||
52.w,
|
||||
textBoxHeight + topPadding + bottomPadding,
|
||||
);
|
||||
|
||||
@ -2797,13 +2799,13 @@ class _MessageItem extends StatelessWidget {
|
||||
required String imageUrl,
|
||||
required TextStyle textStyle,
|
||||
}) {
|
||||
final leftPadding = 22.w;
|
||||
final rightPadding = 24.w;
|
||||
final topPadding = 12.w;
|
||||
final bottomPadding = 16.w;
|
||||
final minSkinWidth = 50.w;
|
||||
final minSkinHeight = 58.w;
|
||||
final textWidthSafety = 10.w;
|
||||
final leftPadding = 16.w;
|
||||
final rightPadding = 18.w;
|
||||
final topPadding = 8.w;
|
||||
final bottomPadding = 10.w;
|
||||
final minSkinWidth = 56.w;
|
||||
final minSkinHeight = 52.w;
|
||||
final textWidthSafety = 4.w;
|
||||
final maxTextWidth = width(220);
|
||||
final maxBubbleWidth = maxTextWidth + leftPadding + rightPadding;
|
||||
final textSize = _measureTextSize(
|
||||
@ -2964,32 +2966,7 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _resolveChatBubbleImageUrl(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((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 "";
|
||||
return scResolveChatBubbleImageUrl(resource);
|
||||
}
|
||||
|
||||
void _showMsgItemMenu(BuildContext ct, String content) {
|
||||
|
||||
@ -1650,9 +1650,9 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
if (!request.isBackpackGiftRequest &&
|
||||
_isInsufficientMoneyError(e, errorMessage)) {
|
||||
RoomGiftComboSendController().hide();
|
||||
RoomRechargeBottomSheet.show(
|
||||
navigatorKey.currentState?.context ?? context,
|
||||
);
|
||||
// RoomRechargeBottomSheet.show(
|
||||
// navigatorKey.currentState?.context ?? context,
|
||||
// );
|
||||
return;
|
||||
}
|
||||
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:provider/provider.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/sc_home_shell_background.dart';
|
||||
|
||||
///Home页面
|
||||
class SCIndexHomePage extends StatefulWidget {
|
||||
@ -69,6 +69,25 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) {
|
||||
@ -87,6 +106,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: SCHomeShellBackground()),
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -94,56 +114,71 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
children: [
|
||||
// 自定义顶部栏(包含状态栏和 TabBar)
|
||||
SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
// TabBar 直接放在这里
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
overlayColor: WidgetStateProperty.all(
|
||||
Colors.transparent,
|
||||
),
|
||||
indicator: const BoxDecoration(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
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,
|
||||
),
|
||||
bottom: false,
|
||||
child: SizedBox(
|
||||
height: 44.w,
|
||||
child: Row(
|
||||
children: [
|
||||
// TabBar 直接放在这里
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
overlayColor: WidgetStateProperty.all(
|
||||
Colors.transparent,
|
||||
),
|
||||
],
|
||||
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 直接作为剩余空间
|
||||
|
||||
@ -3,19 +3,13 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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/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/business_logic/models/res/follow_room_res.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import '../mine/sc_home_mine_room_card.dart';
|
||||
import '../mine/sc_home_mine_skeleton.dart';
|
||||
|
||||
///关注房间
|
||||
///加入的房间
|
||||
class SCRoomFollowPage extends SCPageList {
|
||||
const SCRoomFollowPage({super.key});
|
||||
|
||||
@ -28,7 +22,6 @@ class _RoomFollowPageState
|
||||
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
|
||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||
|
||||
String? lastId;
|
||||
Timer? _roomListRefreshTimer;
|
||||
bool _isSilentRefreshingRooms = false;
|
||||
bool _wasTickerModeEnabled = false;
|
||||
@ -41,10 +34,17 @@ class _RoomFollowPageState
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
enablePullUp = true;
|
||||
enablePullUp = false;
|
||||
backgroundColor = Colors.transparent;
|
||||
isGridView = true;
|
||||
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);
|
||||
_startRoomListAutoRefresh();
|
||||
}
|
||||
@ -97,202 +97,38 @@ class _RoomFollowPageState
|
||||
|
||||
@override
|
||||
Widget buildItem(FollowRoomRes roomRes) {
|
||||
return GestureDetector(
|
||||
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 "";
|
||||
return SCHomeMineRoomCard(room: roomRes);
|
||||
}
|
||||
|
||||
@override
|
||||
empty() {
|
||||
List<Widget> list = [];
|
||||
list.add(SizedBox(height: height(30)));
|
||||
list.add(
|
||||
Image.asset(
|
||||
'sc_images/general/sc_icon_loading.png',
|
||||
width: 120.w,
|
||||
height: 120.w,
|
||||
),
|
||||
);
|
||||
list.add(SizedBox(height: height(15)));
|
||||
list.add(
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.youHaventFollowed,
|
||||
style: TextStyle(
|
||||
fontSize: sp(14),
|
||||
color: Color(0xff999999),
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
height: 1,
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 76.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14.w),
|
||||
border: Border.all(color: const Color(0x3306FFC0)),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0x99111A1B), Color(0x992C4D42)],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
SCAppLocalizations.of(context)!.youHaventJoinedAnyRoomsYet,
|
||||
style: TextStyle(
|
||||
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,
|
||||
Function? onErr,
|
||||
}) async {
|
||||
if (page == 1) {
|
||||
lastId = null;
|
||||
}
|
||||
try {
|
||||
var roomList = await SCAccountRepository().followRoomList(lastId: lastId);
|
||||
if (roomList.isNotEmpty) {
|
||||
lastId = roomList.last.id;
|
||||
}
|
||||
final roomList = await SCAccountRepository().joined();
|
||||
if (page == 1) {
|
||||
_hasCompletedInitialLoad = true;
|
||||
}
|
||||
@ -369,7 +199,6 @@ class _RoomFollowPageState
|
||||
items
|
||||
..clear()
|
||||
..addAll(latestRooms);
|
||||
lastId = latestRooms.isNotEmpty ? latestRooms.last.id : null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -391,7 +220,7 @@ class _RoomFollowPageState
|
||||
}
|
||||
_isSilentRefreshingRooms = true;
|
||||
try {
|
||||
final latestRooms = await SCAccountRepository().followRoomList();
|
||||
final latestRooms = await SCAccountRepository().joined();
|
||||
if (!mounted || !TickerMode.valuesOf(context).enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -4,15 +4,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.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/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/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import '../mine/sc_home_mine_room_card.dart';
|
||||
import '../mine/sc_home_mine_skeleton.dart';
|
||||
|
||||
///历史房间
|
||||
@ -45,6 +39,13 @@ class _SCRoomHistoryPageState
|
||||
backgroundColor = Colors.transparent;
|
||||
isGridView = true;
|
||||
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);
|
||||
_startRoomListAutoRefresh();
|
||||
}
|
||||
@ -97,202 +98,38 @@ class _SCRoomHistoryPageState
|
||||
|
||||
@override
|
||||
Widget buildItem(FollowRoomRes roomRes) {
|
||||
return GestureDetector(
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
return SCHomeMineRoomCard(room: roomRes);
|
||||
}
|
||||
|
||||
@override
|
||||
empty() {
|
||||
List<Widget> list = [];
|
||||
list.add(SizedBox(height: height(30)));
|
||||
list.add(
|
||||
Image.asset(
|
||||
'sc_images/general/sc_icon_loading.png',
|
||||
width: 120.w,
|
||||
height: 120.w,
|
||||
),
|
||||
);
|
||||
list.add(SizedBox(height: height(15)));
|
||||
list.add(
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.noData,
|
||||
style: TextStyle(
|
||||
fontSize: sp(14),
|
||||
color: Color(0xff999999),
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
height: 1,
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 76.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14.w),
|
||||
border: Border.all(color: const Color(0x3306FFC0)),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0x99111A1B), Color(0x992C4D42)],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
SCAppLocalizations.of(context)!.noRoomRecordsRecently,
|
||||
style: TextStyle(
|
||||
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/room/rc_room_manager.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 '../history/sc_room_history_page.dart';
|
||||
import 'sc_home_mine_skeleton.dart';
|
||||
@ -51,72 +51,79 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
||||
super.build(context);
|
||||
_tabs.clear();
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.recent));
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.followed));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Consumer<SocialChatRoomManager>(
|
||||
builder: (_, provider, __) {
|
||||
if (provider.isMyRoomLoading && provider.myRoom == null) {
|
||||
return const SCHomeSkeletonShimmer(
|
||||
builder: _buildMineRoomSkeletonContent,
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.joined));
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionLabel(SCAppLocalizations.of(context)!.myRoom),
|
||||
Consumer<SocialChatRoomManager>(
|
||||
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
|
||||
? GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 10.w),
|
||||
netImage(
|
||||
url: resolveRoomCoverUrl(
|
||||
provider.myRoom?.id,
|
||||
provider.myRoom?.roomCover,
|
||||
child: _buildMineRoomShell(
|
||||
child: Row(
|
||||
children: [
|
||||
netImage(
|
||||
url: resolveRoomCoverUrl(
|
||||
provider.myRoom?.id,
|
||||
provider.myRoom?.roomCover,
|
||||
),
|
||||
defaultImg: kRoomCoverDefaultImg,
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
||||
width: 56.w,
|
||||
height: 56.w,
|
||||
),
|
||||
defaultImg: kRoomCoverDefaultImg,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||
width: 70.w,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
netImage(
|
||||
url:
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
)
|
||||
.findCountryByName(
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.countryName ??
|
||||
"",
|
||||
)
|
||||
?.nationalFlag ??
|
||||
"",
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(3.w),
|
||||
),
|
||||
width: 19.w,
|
||||
height: 14.w,
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
netImage(
|
||||
url:
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
)
|
||||
.findCountryByName(
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.countryName ??
|
||||
"",
|
||||
)
|
||||
?.nationalFlag ??
|
||||
"",
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(3.w),
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 200.w,
|
||||
maxHeight: 24.w,
|
||||
),
|
||||
width: 19.w,
|
||||
height: 14.w,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 20.w,
|
||||
child:
|
||||
(provider.myRoom?.roomName?.length ?? 0) > 18
|
||||
? Marquee(
|
||||
text: provider.myRoom?.roomName ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none,
|
||||
@ -210,32 +214,36 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
text(
|
||||
provider.myRoom?.roomDesc ?? "",
|
||||
fontSize: 14.sp,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
text(
|
||||
"ID:${provider.myRoom?.roomAccount}",
|
||||
fontSize: 12.sp,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
text(
|
||||
provider.myRoom?.roomDesc ?? "",
|
||||
fontSize: 14.sp,
|
||||
textColor: Colors.white.withValues(alpha: 0.72),
|
||||
),
|
||||
text(
|
||||
"ID:${provider.myRoom?.roomAccount}",
|
||||
fontSize: 10.sp,
|
||||
textColor: Colors.white.withValues(alpha: 0.64),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
],
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
size: 26.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
final roomManager = Provider.of<SocialChatRoomManager>(
|
||||
@ -262,51 +270,78 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
||||
},
|
||||
)
|
||||
: GestureDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 10.w),
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_index_creat_room_tag.png",
|
||||
height: 70.w,
|
||||
width: 70.w,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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,
|
||||
),
|
||||
],
|
||||
child: _buildMineRoomShell(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56.w,
|
||||
height: 56.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: scHomeShellAccentColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: Icon(Icons.add, color: Colors.white, size: 28.w),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_my_room_tag2.png",
|
||||
height: 25.w,
|
||||
width: 25.w,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: Colors.white,
|
||||
size: 20.w,
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
],
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.crateMyRoom,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
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: () {
|
||||
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 'package:flutter/material.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:provider/provider.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/text/sc_text.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';
|
||||
|
||||
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
|
||||
@ -28,6 +28,7 @@ const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41);
|
||||
const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53);
|
||||
const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177);
|
||||
const Color _kPartySkeletonBorder = Color(0x66D8B57B);
|
||||
const Color _kPartyAccent = Color(0xFF18F2B1);
|
||||
|
||||
class _RoomCountryFilterOption {
|
||||
const _RoomCountryFilterOption({
|
||||
@ -56,11 +57,20 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||
static const Duration _roomCounterHydrationMinGap = Duration(seconds: 20);
|
||||
static const int _roomCounterHydrationLimit = 6;
|
||||
static const List<String> _topRankBorderAssets = <String>[
|
||||
"sc_images/index/sc_icon_home_room_rank_border_1.svga",
|
||||
"sc_images/index/sc_icon_home_room_rank_border_2.svga",
|
||||
"sc_images/index/sc_icon_home_room_rank_border_3.svga",
|
||||
];
|
||||
static const String _wealthRankBackgroundAsset =
|
||||
'sc_images/index/sc_party_rank_wealth_bg.png';
|
||||
static const String _charmRankBackgroundAsset =
|
||||
'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 = [];
|
||||
String? lastId;
|
||||
@ -140,6 +150,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
},
|
||||
onLoading: () {},
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(top: 10.w, bottom: 10.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -149,7 +160,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12.w),
|
||||
padding: EdgeInsets.only(bottom: 8.w),
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
@ -425,7 +436,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
children: [
|
||||
CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: 95.w,
|
||||
height: 100.w,
|
||||
autoPlay: hasMultipleBanners,
|
||||
// 启用自动播放
|
||||
enlargeCenterPage: false,
|
||||
@ -448,12 +459,20 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
items:
|
||||
banners.map((item) {
|
||||
return GestureDetector(
|
||||
child: netImage(
|
||||
url: item.cover ?? "",
|
||||
height: 95.w,
|
||||
width: ScreenUtil().screenWidth * 0.9,
|
||||
fit: BoxFit.fill,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth - 20.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
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: () {
|
||||
SCBannerUtils.openBanner(item, context);
|
||||
@ -466,13 +485,15 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
children:
|
||||
banners.asMap().entries.map((entry) {
|
||||
return Container(
|
||||
width: 6.0,
|
||||
height: 6.0,
|
||||
margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
||||
width: _currentIndex == entry.key ? 10.w : 4.w,
|
||||
height: 3.w,
|
||||
margin: EdgeInsets.symmetric(vertical: 6.w, horizontal: 2.w),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
color:
|
||||
_currentIndex == entry.key ? Colors.blue : Colors.white,
|
||||
_currentIndex == entry.key
|
||||
? _kPartyAccent
|
||||
: _kPartyAccent.withValues(alpha: 0.45),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@ -579,113 +600,66 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
charmOneAvatar.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 130.w,
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 26.w,
|
||||
top: 65.w,
|
||||
child: head(url: wealthTwoAvatar, width: 30.w),
|
||||
),
|
||||
Positioned(
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildLargeLeaderboardCard(
|
||||
title: SCAppLocalizations.of(context)!.leaderboardWealth,
|
||||
borderColor: const Color(0xFF05C58A),
|
||||
gradientColors: const [Color(0xFF235045), Color(0xFF152121)],
|
||||
imageAsset: _wealthRankBackgroundAsset,
|
||||
avatars: [wealthTwoAvatar, wealthOneAvatar, wealthThreeAvatar],
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false",
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 11.w),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
start: 24.w,
|
||||
top: 68.w,
|
||||
child: head(url: roomGiftTwoAvatar, width: 30.w),
|
||||
),
|
||||
Positioned(
|
||||
top: 35.w,
|
||||
child: head(url: roomGiftOneAvatar, width: 38.w),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 25.w,
|
||||
top: 69.w,
|
||||
child: head(url: roomGiftThreeAvatar, width: 30.w),
|
||||
),
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getPopularLeaderboardBackgroundImage('room'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false",
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
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,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSmallLeaderboardCard(
|
||||
title: SCAppLocalizations.of(context)!.leaderboardCharm,
|
||||
borderColor: const Color(0xFF6800E1),
|
||||
gradientColors: const [Color(0x99632CBA), Color(0x998747CB)],
|
||||
imageAsset: _charmRankBackgroundAsset,
|
||||
imageTop: -3,
|
||||
imageEnd: 13,
|
||||
avatars: [charmTwoAvatar, charmOneAvatar, charmThreeAvatar],
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false",
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
_buildSmallLeaderboardCard(
|
||||
title: SCAppLocalizations.of(context)!.leaderboardRoom,
|
||||
borderColor: const Color(0xFF0A88F5),
|
||||
gradientColors: const [Color(0xCC2169A3), Color(0xCC2F7F9D)],
|
||||
imageAsset: _roomRankBackgroundAsset,
|
||||
imageTop: 2,
|
||||
imageEnd: 8,
|
||||
avatars: [
|
||||
roomGiftTwoAvatar,
|
||||
roomGiftOneAvatar,
|
||||
roomGiftThreeAvatar,
|
||||
],
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&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() {
|
||||
if (!_hasCompletedInitialRoomLoad && isLoading && rooms.isEmpty) {
|
||||
return _buildRoomGridSkeleton();
|
||||
@ -730,9 +919,9 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 172 / 200,
|
||||
mainAxisSpacing: 8.w,
|
||||
crossAxisSpacing: 10.w,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 8.w, 10.w, 10.w),
|
||||
itemCount: visibleRooms.length,
|
||||
@ -751,15 +940,15 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 8.w),
|
||||
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 0),
|
||||
child: SizedBox(
|
||||
height: 26.w,
|
||||
height: 28.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
|
||||
itemCount: countryOptions.length + 1,
|
||||
separatorBuilder: (context, index) => SizedBox(width: 5.w),
|
||||
separatorBuilder: (context, index) => SizedBox(width: 8.w),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return _buildCountryFilterChip(
|
||||
@ -811,37 +1000,26 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
height: 24.w,
|
||||
constraints: BoxConstraints(minWidth: 45.w, maxWidth: 95.w),
|
||||
height: 28.w,
|
||||
constraints: BoxConstraints(minWidth: countryKey == null ? 63.w : 0),
|
||||
alignment: AlignmentDirectional.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors:
|
||||
isSelected
|
||||
? const [Color(0xFF5C4430), Color(0xFF231B14)]
|
||||
: [
|
||||
Colors.black.withValues(alpha: 0.34),
|
||||
const Color(0xFF0F3730).withValues(alpha: 0.38),
|
||||
],
|
||||
),
|
||||
color:
|
||||
isSelected
|
||||
? Colors.transparent
|
||||
: const Color(0xFF06392C).withValues(alpha: 0.86),
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected
|
||||
? const Color(0xFFE8C681)
|
||||
: const Color(0x66D8B57B),
|
||||
color: isSelected ? _kPartyAccent : Colors.transparent,
|
||||
width: 1.w,
|
||||
),
|
||||
boxShadow:
|
||||
isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFE8C681).withValues(alpha: 0.16),
|
||||
blurRadius: 5.w,
|
||||
offset: Offset(0, 2.w),
|
||||
color: _kPartyAccent.withValues(alpha: 0.18),
|
||||
blurRadius: 6.w,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
@ -869,13 +1047,10 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: Directionality.of(context),
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected
|
||||
? const Color(0xFFFFE4A3)
|
||||
: Colors.white.withValues(alpha: 0.92),
|
||||
fontSize: 8.sp,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: _kPartyAccent,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1267,161 +1442,113 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
}
|
||||
|
||||
_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(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(3.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage("sc_images/index/sc_icon_room_bord.png"),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: netImage(
|
||||
url: resolveRoomCoverUrl(res.id, res.roomCover),
|
||||
defaultImg: kRoomCoverDefaultImg,
|
||||
color: const Color(0xFF222222),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
width: 200.w,
|
||||
height: 200.w,
|
||||
border: Border.all(color: _kPartyAccent.withAlpha(51)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: rankInfoHorizontalInset,
|
||||
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(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(width: 10.w),
|
||||
Consumer<SCAppGeneralManager>(
|
||||
builder: (_, provider, __) {
|
||||
return netImage(
|
||||
url:
|
||||
"${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,
|
||||
// ),
|
||||
// ),
|
||||
Positioned.fill(
|
||||
child: netImage(
|
||||
url: resolveRoomCoverUrl(res.id, res.roomCover),
|
||||
defaultImg: kRoomCoverDefaultImg,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
res.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,
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
border: Border.all(
|
||||
color: _kPartyAccent.withValues(alpha: 0.2),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
res.extValues?.existsPassword != true
|
||||
? SizedBox(width: 3.w)
|
||||
: Container(height: 10.w),
|
||||
res.extValues?.existsPassword != true
|
||||
? text(
|
||||
res.displayMemberCount,
|
||||
fontSize: 10.sp,
|
||||
lineHeight: 1,
|
||||
)
|
||||
: Container(height: 10.w),
|
||||
SizedBox(width: 10.w),
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
start: 8.w,
|
||||
bottom: 9.w,
|
||||
child:
|
||||
res.extValues?.existsPassword != true
|
||||
? Row(
|
||||
children: [
|
||||
SCRoomLiveAudioIndicator(
|
||||
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(
|
||||
top: -rankBorderOverflow,
|
||||
left: -rankBorderOverflow,
|
||||
right: -rankBorderOverflow,
|
||||
bottom: -rankBorderOverflow,
|
||||
child: SCSvgaAssetWidget(
|
||||
assetPath: rankBorderAsset,
|
||||
active: true,
|
||||
loop: true,
|
||||
fit: BoxFit.fill,
|
||||
allowDrawingOverflow: true,
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
Consumer<SCAppGeneralManager>(
|
||||
builder: (_, provider, __) {
|
||||
final flagUrl =
|
||||
_trimToNull(res.nationalFlag) ??
|
||||
_trimToNull(
|
||||
provider
|
||||
.findCountryByName(res.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(
|
||||
(res.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(
|
||||
|
||||
@ -15,13 +15,13 @@ import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.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/user_manager.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/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||||
import '../../shared/tools/sc_entry_popup_coordinator.dart';
|
||||
import '../../shared/tools/sc_lk_event_bus.dart';
|
||||
@ -175,7 +175,8 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
if (!mounted && visible) {
|
||||
return;
|
||||
}
|
||||
OverlayManager().setHomeRootTabsVisible(visible);
|
||||
final tabAllowsFloating = _currentIndex == 0 || _currentIndex == 2;
|
||||
OverlayManager().setHomeRootTabsVisible(visible && tabAllowsFloating);
|
||||
if (visible) {
|
||||
unawaited(
|
||||
Provider.of<RtmProvider>(
|
||||
@ -201,58 +202,46 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/index/sc_icon_index_bg.png",
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: _buildCachedBody(),
|
||||
bottomNavigationBar: Container(
|
||||
height: 85.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"sc_images/index/sc_index_bottom_navigation_bar_bg.png",
|
||||
const Positioned.fill(child: SCHomeShellBackground()),
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: _buildCachedBody(),
|
||||
bottomNavigationBar: Builder(
|
||||
builder: (context) {
|
||||
final bottomInset = MediaQuery.viewPaddingOf(context).bottom;
|
||||
return Container(
|
||||
height: 72.w + bottomInset,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF06291F).withValues(alpha: 0.96),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(10.w),
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
elevation: 0,
|
||||
enableFeedback: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedLabelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||
width: 1.w,
|
||||
),
|
||||
left: BorderSide(
|
||||
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||
width: 1.w,
|
||||
),
|
||||
right: BorderSide(
|
||||
color: scHomeShellAccentColor.withValues(alpha: 0.72),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: Color(0xffBF854A),
|
||||
unselectedItemColor: Color(0xffC4C4C4),
|
||||
showUnselectedLabels: true,
|
||||
showSelectedLabels: true,
|
||||
items: _bottomItems,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: _switchBottomTab,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.26),
|
||||
blurRadius: 18.w,
|
||||
offset: Offset(0, -6.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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) {
|
||||
if (AccountStorage().isGuestMode && index != 0) {
|
||||
// 任务、消息和“我的”均为账号能力,游客点击时再引导登录。
|
||||
@ -324,6 +386,7 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
_currentIndexNotifier.value = index;
|
||||
_builtPageIndexes.add(index);
|
||||
});
|
||||
_setHomeRootTabsVisible(_routeObserverRoute?.isCurrent ?? true);
|
||||
}
|
||||
|
||||
void _rebuildBottomItemsIfNeeded() {
|
||||
@ -338,13 +401,11 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
BottomNavigationBarItem(
|
||||
icon: _buildBottomTabIcon(
|
||||
active: false,
|
||||
svgaPath: "sc_images/index/sc_icon_home_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_home_no.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_room_off.png",
|
||||
),
|
||||
activeIcon: _buildBottomTabIcon(
|
||||
active: true,
|
||||
svgaPath: "sc_images/index/sc_icon_home_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_home_en.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_room_on.png",
|
||||
),
|
||||
label: SCAppLocalizations.of(context)!.home,
|
||||
),
|
||||
@ -375,14 +436,12 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: _buildBottomTabIcon(
|
||||
active: false,
|
||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_message_no.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_chat_off.png",
|
||||
),
|
||||
)
|
||||
: _buildBottomTabIcon(
|
||||
active: false,
|
||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_message_no.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_chat_off.png",
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -402,14 +461,12 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: _buildBottomTabIcon(
|
||||
active: true,
|
||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_message_en.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_chat_on.png",
|
||||
),
|
||||
)
|
||||
: _buildBottomTabIcon(
|
||||
active: true,
|
||||
svgaPath: "sc_images/index/sc_icon_message_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_message_en.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_chat_on.png",
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -420,13 +477,11 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
BottomNavigationBarItem(
|
||||
icon: _buildBottomTabIcon(
|
||||
active: false,
|
||||
svgaPath: "sc_images/index/sc_icon_me_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_me_no.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_me_off.png",
|
||||
),
|
||||
activeIcon: _buildBottomTabIcon(
|
||||
active: true,
|
||||
svgaPath: "sc_images/index/sc_icon_me_anim.svga",
|
||||
fallbackPath: "sc_images/index/sc_icon_me_en.png",
|
||||
fallbackPath: "sc_images/index/sc_tab_me_on.png",
|
||||
),
|
||||
label: SCAppLocalizations.of(context)!.me,
|
||||
),
|
||||
@ -614,16 +669,19 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
|
||||
Widget _buildBottomTabIcon({
|
||||
required bool active,
|
||||
required String svgaPath,
|
||||
required String fallbackPath,
|
||||
}) {
|
||||
return SCSvgaAssetWidget(
|
||||
assetPath: svgaPath,
|
||||
width: 35.w,
|
||||
height: 35.w,
|
||||
active: active,
|
||||
loop: false,
|
||||
fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w),
|
||||
return SizedBox(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
fallbackPath,
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -633,11 +691,10 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
builder: (_, claimableCount, __) {
|
||||
final icon = _buildBottomTabIcon(
|
||||
active: active,
|
||||
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
|
||||
fallbackPath:
|
||||
active
|
||||
? "sc_images/index/sc_icon_explore_en.png"
|
||||
: "sc_images/index/sc_icon_explore_no.png",
|
||||
? "sc_images/index/sc_tab_task_on.png"
|
||||
: "sc_images/index/sc_tab_task_off.png",
|
||||
);
|
||||
if (claimableCount <= 0) {
|
||||
return icon;
|
||||
|
||||
@ -211,6 +211,7 @@ class SCMainRoute implements SCIRouterProvider {
|
||||
title: params['title']?.first ?? "",
|
||||
url: decodedUrl,
|
||||
showTitle: params['showTitle']?.first ?? "",
|
||||
safeTop: params['safeTop']?.first == "true",
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@ -138,7 +138,7 @@ class _AllChatPageState extends State<AllChatPage> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||
style: TextStyle(fontSize: 10.sp),
|
||||
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Icon(Icons.chevron_right, size: 10.w),
|
||||
|
||||
@ -137,7 +137,7 @@ class _ChatPageState extends State<ChatPage> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||
style: TextStyle(fontSize: 10.sp),
|
||||
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Icon(Icons.chevron_right, size: 10.w),
|
||||
|
||||
@ -130,7 +130,7 @@ class _GiftChatPageState extends State<GiftChatPage> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||||
style: TextStyle(fontSize: 10.sp),
|
||||
style: TextStyle(fontSize: 10.sp, color: Colors.black),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Icon(Icons.chevron_right, size: 10.w),
|
||||
|
||||
@ -619,15 +619,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
if (path.isEmpty) {
|
||||
return SizedBox(width: width, height: width);
|
||||
}
|
||||
if (path.startsWith("assets/") || path.startsWith("sc_images/")) {
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: width,
|
||||
);
|
||||
}
|
||||
return netImage(url: path, width: width);
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: width,
|
||||
);
|
||||
}
|
||||
|
||||
void _checkStart() {
|
||||
|
||||
@ -175,6 +175,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
void _handleRoomRouteVisible() {
|
||||
_releaseCoveredRouteGuard();
|
||||
_setRoomRouteVisible(true);
|
||||
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||
}
|
||||
|
||||
void _scheduleRoomRouteVisible() {
|
||||
@ -253,9 +254,15 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
void _ensureRoomVisualEffectsEnabled() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (rtcProvider.currenRoom == null ||
|
||||
!_roomRouteVisible ||
|
||||
rtcProvider.roomVisualEffectsEnabled) {
|
||||
if (rtcProvider.currenRoom == null || !_roomRouteVisible) {
|
||||
return;
|
||||
}
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
final needsListenerRebind =
|
||||
rtmProvider.msgFloatingGiftListener != _floatingGiftListener ||
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener !=
|
||||
_luckyGiftRewardTickerListener;
|
||||
if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) {
|
||||
return;
|
||||
}
|
||||
_enableRoomVisualEffects();
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.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/wallet/wallet_route.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/sc_user_counter_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_debounce_widget.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 {
|
||||
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';
|
||||
static const String _inviteActivityUrl =
|
||||
'https://h5.haiyihy.com/app-invite/index.html';
|
||||
static const double _inviteActivityHorizontalScale = 750 / 662;
|
||||
|
||||
Map<String, SCUserCounterRes> _counterMap = {};
|
||||
SCVipStatusRes? _vipStatus;
|
||||
@ -330,56 +331,79 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileSection(profileManager),
|
||||
SizedBox(height: 12.w),
|
||||
_buildStatsCard(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildEntryRow(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildWalletVipRow(profileManager),
|
||||
_buildInviteActivityBanner(),
|
||||
_buildMenuCard2(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildMenuCard([
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.settings,
|
||||
assetIcon: 'sc_images/index/sc_icon_settings.png',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
SettingsRoute.settings,
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.language,
|
||||
assetIcon: 'sc_images/general/sc_icon_setting_language.png',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(context, SCMainRoute.language);
|
||||
},
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const Positioned.fill(child: SCHomeShellBackground()),
|
||||
SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 24.w),
|
||||
child: Column(
|
||||
children: [
|
||||
Consumer<SCAppGeneralManager>(
|
||||
builder: (context, generalManager, child) {
|
||||
final profile = profileManager.currentUserProfile;
|
||||
final country =
|
||||
generalManager.findCountryByName(
|
||||
profile?.countryName ?? '',
|
||||
) ??
|
||||
generalManager.findCountryByCode(
|
||||
profile?.countryCode ?? '',
|
||||
);
|
||||
final flagUrl = country?.nationalFlag?.trim() ?? '';
|
||||
return _buildProfileSection(
|
||||
profileManager,
|
||||
flagUrl: flagUrl,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
_buildStatsCard(),
|
||||
SizedBox(height: 14.w),
|
||||
_buildProfileActionPanel(profileManager),
|
||||
SizedBox(height: 18.w),
|
||||
_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 avatarUrl = profile?.userAvatar?.trim() ?? '';
|
||||
|
||||
return SCDebounceWidget(
|
||||
onTap: () {
|
||||
@ -389,119 +413,202 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
replace: false,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 240.w,
|
||||
height: 130.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
head(
|
||||
url: avatarUrl,
|
||||
width: 72.w,
|
||||
height: 72.w,
|
||||
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: [
|
||||
head(
|
||||
url: profile?.userAvatar ?? '',
|
||||
width: 96.w,
|
||||
height: 96.w,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
headdress: profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: profile?.getHeaddress()?.cover,
|
||||
showDefault: true,
|
||||
Row(
|
||||
children: [
|
||||
if (flagUrl.isNotEmpty) ...[
|
||||
netImage(
|
||||
url: flagUrl,
|
||||
width: 20.w,
|
||||
height: 14.w,
|
||||
fit: BoxFit.cover,
|
||||
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() {
|
||||
return _buildGlassCard(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
|
||||
radius: 8.w,
|
||||
child: Row(
|
||||
return Row(
|
||||
children: [
|
||||
_buildStatItem(
|
||||
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: [
|
||||
_buildStatItem(
|
||||
value: _counterText('INTERVIEW'),
|
||||
label: SCAppLocalizations.of(context)!.vistors,
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors),
|
||||
Positioned.fill(child: _buildActionPanelBackground()),
|
||||
PositionedDirectional(
|
||||
start: 0,
|
||||
end: 0,
|
||||
top: 0,
|
||||
height: 76.w,
|
||||
child: SCDebounceWidget(
|
||||
onTap: _openInviteActivity,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
_buildStatDivider(),
|
||||
_buildStatItem(
|
||||
value: _counterText('SUBSCRIPTION'),
|
||||
label: SCAppLocalizations.of(context)!.following,
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow),
|
||||
PositionedDirectional(
|
||||
start: 11.w,
|
||||
end: 11.w,
|
||||
top: 82.w,
|
||||
child: _buildWalletVipRow(profileManager),
|
||||
),
|
||||
_buildStatDivider(),
|
||||
_buildStatItem(
|
||||
value: _counterText('FANS'),
|
||||
label: SCAppLocalizations.of(context)!.friends,
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans),
|
||||
PositionedDirectional(
|
||||
start: 18.w,
|
||||
end: 18.w,
|
||||
top: 161.w,
|
||||
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() {
|
||||
return Row(
|
||||
children: [
|
||||
_buildEntryItem(
|
||||
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),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.level,
|
||||
iconPath: 'sc_images/index/sc_icon_level.png',
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: SCAppLocalizations.of(context)!.bag,
|
||||
iconPath: 'sc_images/index/sc_icon_bag.png',
|
||||
iconPath: 'sc_images/index/sc_me_entry_bag.png',
|
||||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildEntryItem(
|
||||
title: 'cp',
|
||||
iconPath: 'sc_images/index/sc_icon_cp_entry.png',
|
||||
iconWidth: 47,
|
||||
title: SCAppLocalizations.of(context)!.level,
|
||||
iconPath: 'sc_images/index/sc_me_entry_level.png',
|
||||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||||
),
|
||||
_buildEntryItem(
|
||||
title: 'CP',
|
||||
iconPath: 'sc_images/index/sc_me_entry_cp.png',
|
||||
iconWidth: 46,
|
||||
iconHeight: 50,
|
||||
onTap: _openCpActivity,
|
||||
),
|
||||
@ -526,6 +633,7 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
height: 68.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(color: scHomeShellGoldColor, width: 1.w),
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
@ -565,12 +673,12 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
child: Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
return Text(
|
||||
':${_balanceText(ref.myBalance)}',
|
||||
'${_balanceText(ref.myBalance)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1,
|
||||
@ -780,68 +888,9 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInviteActivityBanner() {
|
||||
return SCDebounceWidget(
|
||||
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>();
|
||||
List<_MenuRowData> _identityMenuItems(
|
||||
SocialChatUserProfileManager userProfile,
|
||||
) {
|
||||
final items = <_MenuRowData>[];
|
||||
final hasCurrentUser = userProfile.currentUserProfile != null;
|
||||
final isAgent = userProfile.userIdentity?.agent ?? false;
|
||||
@ -862,11 +911,11 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
items.add(
|
||||
_MenuRowData(
|
||||
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: () {
|
||||
SCNavigatorUtils.push(
|
||||
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(
|
||||
_MenuRowData(
|
||||
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: () {
|
||||
SCNavigatorUtils.push(
|
||||
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(
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.admin,
|
||||
assetIcon: 'sc_images/index/sc_icon_bd_leader.png',
|
||||
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
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(
|
||||
_MenuRowData(
|
||||
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: () {
|
||||
SCNavigatorUtils.push(
|
||||
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(
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.adminCenter,
|
||||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
||||
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
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(
|
||||
_MenuRowData(
|
||||
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: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
@ -970,58 +1019,73 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
'[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();
|
||||
return _buildGlassCard(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w),
|
||||
radius: 4.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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) {
|
||||
return SCDebounceWidget(
|
||||
onTap: item.onTap,
|
||||
child: SizedBox(
|
||||
height: 46.w,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
item.assetIcon!,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Widget _buildMenuGridIcon(String assetIcon) {
|
||||
if (assetIcon.endsWith('.svg')) {
|
||||
return SvgPicture.asset(
|
||||
assetIcon,
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
return Image.asset(
|
||||
assetIcon,
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
color: Colors.white,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1037,13 +1101,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
child: SCDebounceWidget(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 110.w,
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
height: 88.w,
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
@ -1052,8 +1111,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
children: [
|
||||
Image.asset(
|
||||
iconPath,
|
||||
width: (iconWidth ?? 54).w,
|
||||
height: (iconHeight ?? 54).w,
|
||||
width: (iconWidth ?? 50).w,
|
||||
height: (iconHeight ?? 50).w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
@ -1064,7 +1123,8 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
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(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.w),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: scHomeShellAccentColor.withValues(alpha: 0.7),
|
||||
fontSize: 12.sp,
|
||||
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(
|
||||
width: 0.5.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),
|
||||
);
|
||||
}
|
||||
@ -1163,13 +1226,16 @@ class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
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(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFF0A7D49).withValues(alpha: 0.20),
|
||||
const Color(0xFF031513).withValues(alpha: 0.35),
|
||||
const Color(0xFF111A1B).withValues(alpha: 0.62),
|
||||
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_user_repository_impl.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 {
|
||||
const TaskPage({
|
||||
@ -27,7 +28,11 @@ class TaskPage extends StatefulWidget {
|
||||
|
||||
class _TaskPageState extends State<TaskPage>
|
||||
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 _taskRechargeAsset =
|
||||
'sc_images/index/sc_icon_task_recharge.png';
|
||||
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
|
||||
|
||||
final SCAccountRepository _repository = SCAccountRepository();
|
||||
@ -94,9 +99,6 @@ class _TaskPageState extends State<TaskPage>
|
||||
if (!mounted) return;
|
||||
final sortedTasks =
|
||||
tasks..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||
final dailyCount =
|
||||
sortedTasks.where((task) => (task.taskType ?? 0).toInt() == 0).length;
|
||||
final exclusiveCount = sortedTasks.length - dailyCount;
|
||||
setState(() {
|
||||
_tasks = sortedTasks;
|
||||
_loading = false;
|
||||
@ -133,88 +135,90 @@ class _TaskPageState extends State<TaskPage>
|
||||
final newcomerTasks = _sectionTasks(isNewcomer: true);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF00221D),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
backgroundColor: scHomeShellBackgroundColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: SCHomeShellBackground()),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildTopBar(),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: const Color(0xFF16DBB2),
|
||||
backgroundColor: const Color(0xFF06342A),
|
||||
onRefresh: _handleManualRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 18.w, 10.w, 28.w),
|
||||
child: Column(
|
||||
children: [
|
||||
if (_loading)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 120.w),
|
||||
child: SizedBox(
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Color(0xFF16DBB2),
|
||||
Column(
|
||||
children: [
|
||||
_buildTopBar(),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: const Color(0xFF16DBB2),
|
||||
backgroundColor: const Color(0xFF06342A),
|
||||
onRefresh: _handleManualRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 28.w),
|
||||
child: Column(
|
||||
children: [
|
||||
if (_loading)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 120.w),
|
||||
child: SizedBox(
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
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() {
|
||||
return SizedBox(
|
||||
height: 44.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
height: 48.w,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child:
|
||||
widget.showBackButton
|
||||
? GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(width: 44.w, height: 44.w),
|
||||
),
|
||||
Text(
|
||||
if (widget.showBackButton)
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => SCNavigatorUtils.goBack(context),
|
||||
child: SizedBox(
|
||||
width: 44.w,
|
||||
height: 48.w,
|
||||
child: Icon(
|
||||
SCGlobalConfig.lang == 'ar'
|
||||
? Icons.keyboard_arrow_right
|
||||
: Icons.keyboard_arrow_left,
|
||||
color: Colors.white,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(width: 10.w),
|
||||
SCHomeShellGradientText(
|
||||
SCAppLocalizations.of(context)!.task,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
style: scHomeShellTitleStyle(fontSize: 28.sp),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -517,7 +521,18 @@ class _TaskPageState extends State<TaskPage>
|
||||
}
|
||||
|
||||
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(
|
||||
@ -688,15 +703,16 @@ class _TaskSection extends StatelessWidget {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF052E25),
|
||||
borderRadius: BorderRadius.circular(7.w),
|
||||
border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w),
|
||||
color: const Color(0xFF08251E).withValues(alpha: 0.56),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(color: const Color(0xFF1FA87C), width: 0.5.w),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(child: _GradientTaskTitle(title)),
|
||||
SizedBox(width: 8.w),
|
||||
@ -705,10 +721,10 @@ class _TaskSection extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -726,7 +742,7 @@ class _TaskSection extends StatelessWidget {
|
||||
child: Divider(
|
||||
height: 1.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) {
|
||||
final style = TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
height: 23.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
top: 3.w,
|
||||
start: 0,
|
||||
end: 0,
|
||||
child: Text(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TaskTitleSparkle(),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: SCHomeShellGradientText(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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(
|
||||
children: [
|
||||
SizedBox(width: 12.w),
|
||||
Container(
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x30BEF9E8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: _TaskIcon(task: task),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
SizedBox(width: 42.w, height: 42.w, child: _TaskIcon(task: task)),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -861,36 +836,43 @@ class _TaskRow extends StatelessWidget {
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
if (task.requirementText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.requirementText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
],
|
||||
if (task.progressText.isNotEmpty) ...[
|
||||
Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
] else
|
||||
SizedBox(height: 6.w),
|
||||
SizedBox(height: 4.w),
|
||||
Row(
|
||||
children: [
|
||||
if (task.requirementText.isNotEmpty)
|
||||
Flexible(
|
||||
child: Text(
|
||||
task.requirementText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (task.requirementText.isNotEmpty &&
|
||||
task.progressText.isNotEmpty)
|
||||
SizedBox(width: 10.w),
|
||||
if (task.progressText.isNotEmpty)
|
||||
Flexible(
|
||||
child: Text(
|
||||
task.progressText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF28E883),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
@ -921,7 +903,7 @@ class _TaskRow extends StatelessWidget {
|
||||
claiming: claiming,
|
||||
onTap: () => onAction(task),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
SizedBox(width: 14.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -950,16 +932,26 @@ class _TaskActionButton extends StatelessWidget {
|
||||
onTap: claiming ? null : onTap,
|
||||
child: Container(
|
||||
width: config.width.w,
|
||||
height: 20.w,
|
||||
height: 24.w,
|
||||
alignment: Alignment.center,
|
||||
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:
|
||||
config.borderColor == null
|
||||
config.assetPath != null || config.borderColor == null
|
||||
? null
|
||||
: Border.all(color: config.borderColor!, width: 1.w),
|
||||
gradient: config.gradient,
|
||||
color: config.gradient == null ? config.backgroundColor : null,
|
||||
gradient: config.assetPath == null ? config.gradient : null,
|
||||
color:
|
||||
config.assetPath == null && config.gradient == null
|
||||
? config.backgroundColor
|
||||
: null,
|
||||
),
|
||||
child:
|
||||
claiming
|
||||
@ -977,7 +969,7 @@ class _TaskActionButton extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: config.textColor,
|
||||
fontSize: 13.sp,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
@ -1000,8 +992,8 @@ class _TaskIcon extends StatelessWidget {
|
||||
}
|
||||
return Image.network(
|
||||
iconUrl,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _buildAssetIcon(),
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
@ -1014,8 +1006,8 @@ class _TaskIcon extends StatelessWidget {
|
||||
Widget _buildAssetIcon() {
|
||||
return Image.asset(
|
||||
task.iconAsset,
|
||||
width: 29.4.w,
|
||||
height: 29.4.w,
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
@ -1029,6 +1021,7 @@ class _TaskButtonConfig {
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.gradient,
|
||||
this.assetPath,
|
||||
});
|
||||
|
||||
final String label;
|
||||
@ -1037,6 +1030,7 @@ class _TaskButtonConfig {
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
final Gradient? gradient;
|
||||
final String? assetPath;
|
||||
|
||||
factory _TaskButtonConfig.fromStatus(
|
||||
_TaskActionStatus status,
|
||||
@ -1046,8 +1040,9 @@ class _TaskButtonConfig {
|
||||
case _TaskActionStatus.claim:
|
||||
return _TaskButtonConfig(
|
||||
label: l10n.claim,
|
||||
textColor: Color(0xFF2B1500),
|
||||
width: 53,
|
||||
textColor: Color(0xFF221901),
|
||||
width: 66,
|
||||
assetPath: 'sc_images/index/sc_task_action_claim_bg.png',
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
@ -1058,7 +1053,8 @@ class _TaskButtonConfig {
|
||||
return _TaskButtonConfig(
|
||||
label: l10n.claimed,
|
||||
textColor: Colors.white,
|
||||
width: 58,
|
||||
width: 66,
|
||||
assetPath: 'sc_images/index/sc_task_action_claimed_bg.png',
|
||||
backgroundColor: const Color(0xFF062E28),
|
||||
borderColor: Colors.white.withValues(alpha: 0.56),
|
||||
);
|
||||
@ -1066,7 +1062,8 @@ class _TaskButtonConfig {
|
||||
return _TaskButtonConfig(
|
||||
label: l10n.go,
|
||||
textColor: Colors.white,
|
||||
width: 53,
|
||||
width: 66,
|
||||
assetPath: 'sc_images/index/sc_task_action_go_bg.png',
|
||||
backgroundColor: const Color(0xFF062E28),
|
||||
borderColor: const Color(0xFF98E4C2),
|
||||
);
|
||||
|
||||
@ -27,11 +27,13 @@ class WebViewPage extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
final String showTitle;
|
||||
final bool safeTop;
|
||||
|
||||
const WebViewPage({
|
||||
Key? key,
|
||||
this.title = "",
|
||||
this.showTitle = "true",
|
||||
this.safeTop = false,
|
||||
required this.url,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -293,7 +295,7 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
)
|
||||
: null,
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
top: widget.safeTop,
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
|
||||
@ -2557,14 +2557,41 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> releaseRtcEngineForAppTermination() async {
|
||||
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
final shouldQuitRoom = !_currentRoomIsEntryPreview && roomId.isNotEmpty;
|
||||
final adapter = _roomRtcEngineAdapter;
|
||||
_roomRtcEngineAdapter = null;
|
||||
_rtcEnginePrewarmTask = null;
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
_resetRoomRtcTracking();
|
||||
try {
|
||||
await adapter?.release();
|
||||
} catch (e) {}
|
||||
|
||||
final rtcReleaseTask = () async {
|
||||
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 remoteCleanupTask;
|
||||
}
|
||||
|
||||
void _handleRoomRtcVoiceVolume(
|
||||
@ -4172,7 +4199,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final currentPercent = _roomRocketStatusPercent(currentStatus);
|
||||
if (currentStatus.currentEnergy == 0 &&
|
||||
currentPercent <= 0 &&
|
||||
currentLevel >= launch.safeLevel) {
|
||||
currentLevel > launch.safeLevel) {
|
||||
return;
|
||||
}
|
||||
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_relation_broadcast_assets.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_user_id_utils.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/repositories/sc_user_repository_impl.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:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart';
|
||||
@ -95,6 +97,159 @@ typedef OnNewGroupMessageListener =
|
||||
typedef OnMessageRecvC2CReadListener = Function(List<String> messageIDList);
|
||||
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 }
|
||||
|
||||
class _LuckGiftPushQueueEntry {
|
||||
@ -217,6 +372,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _luckyGiftBurstRecentEventTtlMs = 5000;
|
||||
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
|
||||
static const int _cpRelationGiftRefreshMinIntervalMs = 800;
|
||||
static const Duration _roomContributionGiftRefreshDebounce = Duration(
|
||||
milliseconds: 500,
|
||||
);
|
||||
static const List<Duration> _cpRelationGiftRefreshDelays = [
|
||||
Duration(milliseconds: 300),
|
||||
Duration(milliseconds: 1500),
|
||||
@ -358,6 +516,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
|
||||
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
|
||||
int _lastCpRelationGiftRefreshAt = 0;
|
||||
Timer? _roomContributionGiftRefreshTimer;
|
||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||
String? roomRedPacketBroadcastGroupId;
|
||||
String? roomRedPacketBroadcastRegionCode;
|
||||
@ -1472,15 +1631,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
if (result == _CpInviteProcessResult.accepted) {
|
||||
if (isSender) {
|
||||
_dispatchCpRelationEstablishedRoomNotice(
|
||||
groupId: _currentVisibleVoiceRoomGroupId(context),
|
||||
applyId: applyId,
|
||||
data: data,
|
||||
invite: invite,
|
||||
currentProfile: currentProfile,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (result == _CpInviteProcessResult.expired) {
|
||||
@ -1793,15 +1943,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
|
||||
unawaited(profileManager.refreshLoadedCpProfiles());
|
||||
if (agree) {
|
||||
_dispatchCpRelationEstablishedRoomNotice(
|
||||
groupId: voiceRoomGroupId,
|
||||
applyId: applyId,
|
||||
data: data,
|
||||
invite: invite,
|
||||
currentProfile: AccountStorage().getCurrentUser()?.userProfile,
|
||||
);
|
||||
} else {
|
||||
if (!agree) {
|
||||
_dispatchCpInviteRejectedRoomNotice(
|
||||
groupId: voiceRoomGroupId,
|
||||
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({
|
||||
required String groupId,
|
||||
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(
|
||||
Iterable<SocialChatUserProfile?> profiles,
|
||||
) {
|
||||
@ -2606,6 +2667,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
String msg,
|
||||
V2TimConversation toConversation,
|
||||
) async {
|
||||
final chatBubbleCloudData = scEncodeChatBubbleMessageSnapshot(
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getChatBox(),
|
||||
);
|
||||
// 创建文本消息
|
||||
V2TimValueCallback<V2TimMsgCreateInfoResult> createTextMessageRes =
|
||||
await TencentImSDKPlugin.v2TIMManager
|
||||
@ -2628,6 +2692,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
id: id, // 创建的messageid
|
||||
receiver: toConversation.userID!, // 接收人id
|
||||
needReadReceipt: true,
|
||||
cloudCustomData: chatBubbleCloudData,
|
||||
groupID: '', // 是否需要已读回执
|
||||
);
|
||||
if (sendMessageRes.code == 0) {
|
||||
@ -2973,6 +3038,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
addMsg(Msg msg) {
|
||||
_scheduleRoomRocketStatusRefreshForGiftMessage(msg);
|
||||
_scheduleCpRelationRefreshForGiftMessage(msg);
|
||||
_scheduleRoomContributionRefreshForGiftMessage(msg);
|
||||
final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg);
|
||||
if (mergedGiftMsg != null) {
|
||||
msgAllListener?.call(mergedGiftMsg);
|
||||
@ -2997,6 +3063,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (SCRoomMsgType.isRocketRewardUserType(msg.type)) {
|
||||
return;
|
||||
}
|
||||
if (shouldSuppressRoomCpRelationNotice(
|
||||
messageType: msg.type,
|
||||
noticeAction: msg.noticeAction,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.rocketLaunchNotice &&
|
||||
_hasRoomRocketNoticeMessage(msg)) {
|
||||
return;
|
||||
@ -3111,6 +3183,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
giftTab: relationType,
|
||||
displayText: msg.msg ?? "",
|
||||
durationSeconds: 5,
|
||||
gameId: msg.giftBatchId,
|
||||
broadcastScope:
|
||||
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) {
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentProfile == null ||
|
||||
@ -3355,6 +3457,43 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
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) {
|
||||
if (!_isCpRelationGiftMessage(msg)) {
|
||||
return;
|
||||
@ -4196,7 +4335,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
try {
|
||||
final decoded = json.decode(customData);
|
||||
final payload = _broadcastPayloadMap(decoded);
|
||||
return payload['type'] == SCRoomMsgType.roomRedPacket;
|
||||
return payload['type'] == SCRoomMsgType.roomRedPacket ||
|
||||
payload['type'] == SCRoomMsgType.cpRelationBroadcast;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@ -6077,34 +6217,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (isRegionBroadcastGroup) {
|
||||
_handleRegionGiftBroadcast(data["data"]);
|
||||
}
|
||||
} else if (type == SCRoomMsgType.cpSystemNotice) {
|
||||
final payload = data["data"] ?? data;
|
||||
final payloadMap = _broadcastPayloadMap(payload);
|
||||
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,
|
||||
} else if (type == SCRoomMsgType.cpRelationBroadcast) {
|
||||
final floatingMessage = buildCpRelationBroadcastFloatingMessage(
|
||||
data["data"] ?? data,
|
||||
);
|
||||
if (floatingMessage != null) {
|
||||
OverlayManager().addMessage(floatingMessage);
|
||||
_showCpRelationFormedDialogFromBroadcast(floatingMessage);
|
||||
}
|
||||
} else if (type == SCRoomMsgType.inviteRoom) {
|
||||
///邀请进入房间
|
||||
var fdata = data["data"];
|
||||
@ -6820,6 +6940,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hiddenCurrentRoomRocketLaunchKeys.clear();
|
||||
_roomRocketRewardPopupRetryTimer?.cancel();
|
||||
_roomRocketRewardPopupRetryTimer = null;
|
||||
_roomContributionGiftRefreshTimer?.cancel();
|
||||
_roomContributionGiftRefreshTimer = null;
|
||||
for (final timer in _roomRocketRewardHoldTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
List<CountryMode> countryModeList = [];
|
||||
final Map<String, List<Country>> _countryMap = {};
|
||||
final Map<String, Country> _countryByNameMap = {};
|
||||
final Map<String, Country> _countryByCodeMap = {};
|
||||
bool _isFetchingCountryList = false;
|
||||
|
||||
///礼物
|
||||
@ -114,6 +115,7 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
final result = await SCConfigRepositoryImp().loadCountry();
|
||||
_countryMap.clear();
|
||||
_countryByNameMap.clear();
|
||||
_countryByCodeMap.clear();
|
||||
countryModeList.clear();
|
||||
|
||||
for (final c in result.openCountry ?? const <Country>[]) {
|
||||
@ -126,6 +128,10 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
if ((c.countryName ?? "").isNotEmpty) {
|
||||
_countryByNameMap[c.countryName!] = c;
|
||||
}
|
||||
final countryCode = c.alphaTwo?.trim().toUpperCase() ?? "";
|
||||
if (countryCode.isNotEmpty) {
|
||||
_countryByCodeMap[countryCode] = c;
|
||||
}
|
||||
}
|
||||
|
||||
_countryMap.forEach((k, v) {
|
||||
@ -147,6 +153,10 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
return _countryByNameMap[countryName];
|
||||
}
|
||||
|
||||
Country? findCountryByCode(String countryCode) {
|
||||
return _countryByCodeMap[countryCode.trim().toUpperCase()];
|
||||
}
|
||||
|
||||
///选择国家
|
||||
Country? chooseCountry(int subIndex, int cIndex) {
|
||||
var selectCm = countryModeList[subIndex].prefixCountrys[cIndex];
|
||||
@ -468,14 +478,41 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Map<String, List<Emojis>> emojiByTab = {};
|
||||
List<SCRoomEmojiRes> roomEmojiGroups = [];
|
||||
bool _isFetchingRoomEmojis = false;
|
||||
bool _hasRoomEmojisLoaded = false;
|
||||
|
||||
bool get isFetchingRoomEmojis => _isFetchingRoomEmojis;
|
||||
|
||||
bool get hasRoomEmojisLoaded => _hasRoomEmojisLoaded;
|
||||
|
||||
///emoji表情
|
||||
void emojiAll() async {
|
||||
var roomEmojis = await SCChatRoomRepository().emojiAll();
|
||||
for (var value in roomEmojis) {
|
||||
emojiByTab[value.groupName!] = value.emojis ?? [];
|
||||
Future<void> emojiAll() async {
|
||||
if (_isFetchingRoomEmojis || _hasRoomEmojisLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
_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
|
||||
/// sysOrigin : ""
|
||||
/// groupCode : ""
|
||||
/// groupName : ""
|
||||
/// cover : ""
|
||||
/// have : false
|
||||
/// amount : 0.0
|
||||
/// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
||||
|
||||
class SCRoomEmojiRes {
|
||||
SCRoomEmojiRes({
|
||||
String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,}){
|
||||
_id = id;
|
||||
_sysOrigin = sysOrigin;
|
||||
_groupCode = groupCode;
|
||||
_groupName = groupName;
|
||||
_cover = cover;
|
||||
_have = have;
|
||||
_amount = amount;
|
||||
_emojis = emojis;
|
||||
}
|
||||
|
||||
SCRoomEmojiRes.fromJson(dynamic json) {
|
||||
_id = json['id'];
|
||||
_sysOrigin = json['sysOrigin'];
|
||||
_groupCode = json['groupCode'];
|
||||
_groupName = json['groupName'];
|
||||
_cover = json['cover'];
|
||||
_have = json['have'];
|
||||
_amount = json['amount'];
|
||||
if (json['emojis'] != null) {
|
||||
_emojis = [];
|
||||
json['emojis'].forEach((v) {
|
||||
_emojis?.add(Emojis.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? _id;
|
||||
String? _sysOrigin;
|
||||
String? _groupCode;
|
||||
String? _groupName;
|
||||
String? _cover;
|
||||
bool? _have;
|
||||
num? _amount;
|
||||
List<Emojis>? _emojis;
|
||||
SCRoomEmojiRes copyWith({ String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,
|
||||
}) => SCRoomEmojiRes( id: id ?? _id,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
groupCode: groupCode ?? _groupCode,
|
||||
groupName: groupName ?? _groupName,
|
||||
cover: cover ?? _cover,
|
||||
have: have ?? _have,
|
||||
amount: amount ?? _amount,
|
||||
emojis: emojis ?? _emojis,
|
||||
);
|
||||
String? get id => _id;
|
||||
String? get sysOrigin => _sysOrigin;
|
||||
String? get groupCode => _groupCode;
|
||||
String? get groupName => _groupName;
|
||||
String? get cover => _cover;
|
||||
bool? get have => _have;
|
||||
num? get amount => _amount;
|
||||
List<Emojis>? get emojis => _emojis;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
map['groupCode'] = _groupCode;
|
||||
map['groupName'] = _groupName;
|
||||
map['cover'] = _cover;
|
||||
map['have'] = _have;
|
||||
map['amount'] = _amount;
|
||||
if (_emojis != null) {
|
||||
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// sourceUrl : ""
|
||||
/// sourceType : 0
|
||||
/// coverUrl : ""
|
||||
/// type : ""
|
||||
|
||||
class Emojis {
|
||||
Emojis({
|
||||
String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,}){
|
||||
_sourceUrl = sourceUrl;
|
||||
_sourceType = sourceType;
|
||||
_coverUrl = coverUrl;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
Emojis.fromJson(dynamic json) {
|
||||
_sourceUrl = json['sourceUrl'];
|
||||
_sourceType = json['sourceType'];
|
||||
_coverUrl = json['coverUrl'];
|
||||
_type = json['type'];
|
||||
}
|
||||
String? _sourceUrl;
|
||||
num? _sourceType;
|
||||
String? _coverUrl;
|
||||
String? _type;
|
||||
Emojis copyWith({ String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,
|
||||
}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl,
|
||||
sourceType: sourceType ?? _sourceType,
|
||||
coverUrl: coverUrl ?? _coverUrl,
|
||||
type: type ?? _type,
|
||||
);
|
||||
String? get sourceUrl => _sourceUrl;
|
||||
num? get sourceType => _sourceType;
|
||||
String? get coverUrl => _coverUrl;
|
||||
String? get type => _type;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['sourceUrl'] = _sourceUrl;
|
||||
map['sourceType'] = _sourceType;
|
||||
map['coverUrl'] = _coverUrl;
|
||||
map['type'] = _type;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
// id : 0
|
||||
// sysOrigin : ""
|
||||
// groupCode : ""
|
||||
// groupName : ""
|
||||
// cover : ""
|
||||
// have : false
|
||||
// amount : 0.0
|
||||
// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
||||
|
||||
class SCRoomEmojiRes {
|
||||
SCRoomEmojiRes({
|
||||
String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,}){
|
||||
_id = id;
|
||||
_sysOrigin = sysOrigin;
|
||||
_groupCode = groupCode;
|
||||
_groupName = groupName;
|
||||
_cover = cover;
|
||||
_have = have;
|
||||
_amount = amount;
|
||||
_emojis = emojis;
|
||||
}
|
||||
|
||||
SCRoomEmojiRes.fromJson(dynamic json) {
|
||||
final map = json is Map ? json : const <String, dynamic>{};
|
||||
_id = _stringOrNull(map['id']);
|
||||
_sysOrigin = _stringOrNull(map['sysOrigin']);
|
||||
_groupCode = _stringOrNull(map['groupCode']);
|
||||
_groupName = _stringOrNull(map['groupName']);
|
||||
_cover = _stringOrNull(map['cover']);
|
||||
_have = _boolOrNull(map['have']);
|
||||
_amount = _numOrNull(map['amount']);
|
||||
if (map['emojis'] is List) {
|
||||
_emojis = [];
|
||||
map['emojis'].forEach((v) {
|
||||
_emojis?.add(Emojis.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? _id;
|
||||
String? _sysOrigin;
|
||||
String? _groupCode;
|
||||
String? _groupName;
|
||||
String? _cover;
|
||||
bool? _have;
|
||||
num? _amount;
|
||||
List<Emojis>? _emojis;
|
||||
SCRoomEmojiRes copyWith({ String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,
|
||||
}) => SCRoomEmojiRes( id: id ?? _id,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
groupCode: groupCode ?? _groupCode,
|
||||
groupName: groupName ?? _groupName,
|
||||
cover: cover ?? _cover,
|
||||
have: have ?? _have,
|
||||
amount: amount ?? _amount,
|
||||
emojis: emojis ?? _emojis,
|
||||
);
|
||||
String? get id => _id;
|
||||
String? get sysOrigin => _sysOrigin;
|
||||
String? get groupCode => _groupCode;
|
||||
String? get groupName => _groupName;
|
||||
String? get cover => _cover;
|
||||
bool? get have => _have;
|
||||
num? get amount => _amount;
|
||||
List<Emojis>? get emojis => _emojis;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
map['groupCode'] = _groupCode;
|
||||
map['groupName'] = _groupName;
|
||||
map['cover'] = _cover;
|
||||
map['have'] = _have;
|
||||
map['amount'] = _amount;
|
||||
if (_emojis != null) {
|
||||
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// sourceUrl : ""
|
||||
/// sourceType : 0
|
||||
/// coverUrl : ""
|
||||
/// type : ""
|
||||
|
||||
class Emojis {
|
||||
Emojis({
|
||||
String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,}){
|
||||
_sourceUrl = sourceUrl;
|
||||
_sourceType = sourceType;
|
||||
_coverUrl = coverUrl;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
Emojis.fromJson(dynamic json) {
|
||||
final map = json is Map ? json : const <String, dynamic>{};
|
||||
_sourceUrl = _stringOrNull(map['sourceUrl']);
|
||||
_sourceType = _numOrNull(map['sourceType']);
|
||||
_coverUrl = _stringOrNull(map['coverUrl']);
|
||||
_type = _stringOrNull(map['type']);
|
||||
}
|
||||
String? _sourceUrl;
|
||||
num? _sourceType;
|
||||
String? _coverUrl;
|
||||
String? _type;
|
||||
Emojis copyWith({ String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,
|
||||
}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl,
|
||||
sourceType: sourceType ?? _sourceType,
|
||||
coverUrl: coverUrl ?? _coverUrl,
|
||||
type: type ?? _type,
|
||||
);
|
||||
String? get sourceUrl => _sourceUrl;
|
||||
num? get sourceType => _sourceType;
|
||||
String? get coverUrl => _coverUrl;
|
||||
String? get type => _type;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['sourceUrl'] = _sourceUrl;
|
||||
map['sourceType'] = _sourceType;
|
||||
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;
|
||||
|
||||
@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 {
|
||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||
@ -48,6 +103,8 @@ class OverlayManager {
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
bool _homeRootTabsVisible = false;
|
||||
String? _currentPageRouteName;
|
||||
bool _allowedPageRefreshScheduled = false;
|
||||
int _suppressedCount = 0;
|
||||
|
||||
bool _isProcessing = false;
|
||||
@ -184,6 +241,10 @@ class OverlayManager {
|
||||
return 'global_lucky_gift_win_fallback|$roomId|$userId|$toUserId|$giftId|$coins|$multiple';
|
||||
}
|
||||
if (message.type == 6) {
|
||||
final eventId = message.gameId?.trim() ?? '';
|
||||
if (eventId.isNotEmpty) {
|
||||
return 'cp_relation_event|$eventId';
|
||||
}
|
||||
final roomId = message.roomId?.trim() ?? '';
|
||||
final action = message.giftId?.trim() ?? '';
|
||||
final relationType = message.giftTab?.trim() ?? '';
|
||||
@ -435,58 +496,68 @@ class OverlayManager {
|
||||
SCFloatingMessage message,
|
||||
VoidCallback onComplete,
|
||||
) {
|
||||
final Widget child;
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
return FloatingLuckGiftScreenWidget(
|
||||
child = FloatingLuckGiftScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
//房间礼物
|
||||
return FloatingGiftScreenWidget(
|
||||
child = FloatingGiftScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
//游戏中奖
|
||||
return FloatingGameScreenWidget(
|
||||
child = FloatingGameScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case FloatingLuckyGiftGlobalScreenWidget.floatingType:
|
||||
// Global lucky gift win.
|
||||
return FloatingLuckyGiftGlobalScreenWidget(
|
||||
child = FloatingLuckyGiftGlobalScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
//火箭
|
||||
return FloatingRoomRocketScreenWidget(
|
||||
child = FloatingRoomRocketScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 4:
|
||||
//红包
|
||||
return FloatingRoomRedenvelopeScreenWidget(
|
||||
child = FloatingRoomRedenvelopeScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 5:
|
||||
//VIP进房飘窗
|
||||
return FloatingVipEntryScreenWidget(
|
||||
child = FloatingVipEntryScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
case 6:
|
||||
//CP / 挚友关系飘窗
|
||||
return FloatingCpRelationScreenWidget(
|
||||
child = FloatingCpRelationScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
onComplete();
|
||||
return Container();
|
||||
child = const SizedBox.shrink();
|
||||
}
|
||||
return _SwipeToDismissFloating(onDismiss: onComplete, child: child);
|
||||
}
|
||||
|
||||
int? _stackSlotKeyFor(SCFloatingMessage message) {
|
||||
@ -616,9 +687,39 @@ class OverlayManager {
|
||||
return;
|
||||
}
|
||||
_homeRootTabsVisible = visible;
|
||||
if (!visible) {
|
||||
_removeActiveRegionBroadcastMessage();
|
||||
_removeRegionBroadcastMessages();
|
||||
_scheduleAllowedPageVisibilityRefresh();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
_restoreDeferredRegionRedPacketMessages();
|
||||
@ -672,6 +773,8 @@ class OverlayManager {
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_homeRootTabsVisible = false;
|
||||
_currentPageRouteName = null;
|
||||
_allowedPageRefreshScheduled = false;
|
||||
_suppressedCount = 0;
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
@ -846,6 +949,9 @@ class OverlayManager {
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
if (!_isAllowedPageVisible) {
|
||||
return false;
|
||||
}
|
||||
if (message.isRegionBroadcast) {
|
||||
return _shouldDisplayRegionBroadcastMessage(context, message);
|
||||
}
|
||||
@ -897,6 +1003,9 @@ class OverlayManager {
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
if (!_isAllowedPageVisible) {
|
||||
return false;
|
||||
}
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -937,13 +1046,13 @@ class OverlayManager {
|
||||
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return true;
|
||||
}
|
||||
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
||||
return SCGlobalConfig.isFloatingBroadcastAll && _isAllowedPageVisible;
|
||||
}
|
||||
if (isGlobalStackedBroadcast && rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return true;
|
||||
}
|
||||
if (isGlobalStackedBroadcast) {
|
||||
return _homeRootTabsVisible;
|
||||
return _isAllowedPageVisible;
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||
return false;
|
||||
@ -951,37 +1060,7 @@ class OverlayManager {
|
||||
if (rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return false;
|
||||
}
|
||||
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
||||
}
|
||||
|
||||
void _removeRegionBroadcastMessages() {
|
||||
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.removeFirst();
|
||||
if (!message.isRegionBroadcast ||
|
||||
_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();
|
||||
return SCGlobalConfig.isFloatingBroadcastAll && _isAllowedPageVisible;
|
||||
}
|
||||
|
||||
void _removeActiveRoomMessage({
|
||||
@ -1065,3 +1144,57 @@ class OverlayManager {
|
||||
_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 {
|
||||
try {
|
||||
await initialize();
|
||||
// 主动登录前清理上次账号,确保重新显示账号选择器
|
||||
await _g.signOut();
|
||||
await FirebaseAuth.instance.signOut();
|
||||
// 触发谷歌登录
|
||||
final GoogleSignInAccount? googleAccount = await _g.signIn();
|
||||
if (googleAccount == null) return null;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.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/shared/data_sources/sources/local/user_manager.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_text_elem.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/text/sc_text.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.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/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/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
||||
@ -46,6 +44,7 @@ class _MessageConversationListPageState
|
||||
extends State<MessageConversationListPage> {
|
||||
final Map<String, SocialChatUserProfile> _conversationUserCache = {};
|
||||
final Set<String> _loadingConversationUserIds = {};
|
||||
String? _deleteActionConversationId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -69,177 +68,199 @@ class _MessageConversationListPageState
|
||||
if (searchQuery.isNotEmpty) {
|
||||
_loadSearchUserInfo(provider.conversationList);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
searchQuery.isEmpty &&
|
||||
provider.customerInfo != null &&
|
||||
provider.customerInfo?.id !=
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.id
|
||||
? Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 15.w,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_logo.png",
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _dismissDeleteAction,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
searchQuery.isEmpty &&
|
||||
provider.customerInfo != null &&
|
||||
provider.customerInfo?.id !=
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.id
|
||||
? Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 15.w,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_logo.png",
|
||||
height: 48.w,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
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(
|
||||
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,
|
||||
textColor: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
child: text(
|
||||
SCAppLocalizations.of(
|
||||
onTap: () async {
|
||||
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,
|
||||
)!.contactUs,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
"${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}",
|
||||
);
|
||||
},
|
||||
),
|
||||
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(
|
||||
bottom: 0,
|
||||
end: 10.w,
|
||||
child: Visibility(
|
||||
visible: provider.customerUnReadCount > 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.customerUnReadCount > 99 ? '99+' : provider.customerUnReadCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
// 未读数量
|
||||
PositionedDirectional(
|
||||
bottom: 0,
|
||||
end: 10.w,
|
||||
child: Visibility(
|
||||
visible: provider.customerUnReadCount > 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.customerUnReadCount > 99 ? '99+' : provider.customerUnReadCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _msgListView(
|
||||
conversationList,
|
||||
isRoom: widget.isRoom,
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: _msgListView(
|
||||
conversationList,
|
||||
isRoom: widget.isRoom,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _dismissDeleteAction() {
|
||||
if (_deleteActionConversationId == null) {
|
||||
return false;
|
||||
}
|
||||
setState(() {
|
||||
_deleteActionConversationId = null;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget _msgListView(
|
||||
List<V2TimConversation> chatList, {
|
||||
EdgeInsetsGeometry? margin,
|
||||
@ -269,9 +290,9 @@ class _MessageConversationListPageState
|
||||
),
|
||||
separatorBuilder:
|
||||
(_, index) => Divider(
|
||||
color: Colors.black12,
|
||||
indent: 65.w,
|
||||
endIndent: 15.w,
|
||||
color: const Color(0xFF0E5B42).withValues(alpha: 0.7),
|
||||
indent: 64.w,
|
||||
endIndent: 0,
|
||||
height: 0.5.w,
|
||||
thickness: 0.5.w,
|
||||
),
|
||||
@ -291,6 +312,28 @@ class _MessageConversationListPageState
|
||||
index: index,
|
||||
isRoom: isRoom,
|
||||
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),
|
||||
);
|
||||
}
|
||||
@ -384,6 +427,11 @@ class ConversationItem extends StatefulWidget {
|
||||
final int index;
|
||||
final bool isRoom;
|
||||
final SocialChatUserProfile? initialUser;
|
||||
final bool isDeleteActionVisible;
|
||||
final bool isAnyDeleteActionVisible;
|
||||
final VoidCallback onDismissDeleteAction;
|
||||
final VoidCallback onLongPress;
|
||||
final Future<void> Function() onDelete;
|
||||
|
||||
const ConversationItem({
|
||||
super.key,
|
||||
@ -391,6 +439,11 @@ class ConversationItem extends StatefulWidget {
|
||||
required this.index,
|
||||
required this.isRoom,
|
||||
this.initialUser,
|
||||
required this.isDeleteActionVisible,
|
||||
required this.isAnyDeleteActionVisible,
|
||||
required this.onDismissDeleteAction,
|
||||
required this.onLongPress,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -526,6 +579,10 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
}*/
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (widget.isAnyDeleteActionVisible) {
|
||||
widget.onDismissDeleteAction();
|
||||
return;
|
||||
}
|
||||
if (_isC2CConversation && _effectiveUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@ -551,9 +608,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
"$route?conversation=${Uri.encodeComponent(json)}",
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
showDeleteConfirm();
|
||||
},
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
//color: Colors.white,
|
||||
@ -562,15 +617,19 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
children: [
|
||||
Container(
|
||||
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(
|
||||
//color: Colors.white,
|
||||
color:
|
||||
widget.isDeleteActionVisible
|
||||
? Colors.white.withValues(alpha: 0.06)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
head(url: _displayAvatar, width: 45.w),
|
||||
SizedBox(width: 6.w),
|
||||
head(url: _displayAvatar, width: 56.w),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -585,11 +644,8 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
maxWidth: 152.w,
|
||||
_displayName,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
textColor:
|
||||
widget.isRoom
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
textColor: Colors.white,
|
||||
type:
|
||||
_isC2CConversation
|
||||
? user?.getVIP()?.name ?? ""
|
||||
@ -602,8 +658,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
SCMDateUtils.formatMessageTime(
|
||||
context,
|
||||
SCConversationTimeFormatter.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
(conversation.lastMessage!.timestamp ?? 0) *
|
||||
1000,
|
||||
@ -611,10 +666,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color:
|
||||
widget.isRoom
|
||||
? Colors.white54
|
||||
: Color(0xff8d8d8d),
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
height: 1,
|
||||
@ -632,10 +684,7 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(13),
|
||||
color:
|
||||
widget.isRoom
|
||||
? Colors.white54
|
||||
: Color(0xFF666666),
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -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() {
|
||||
if (!_shouldLoadUserInfo) return;
|
||||
final userId = _effectiveUserId;
|
||||
|
||||
@ -368,11 +368,13 @@ class _RoomCpInviteDialogState extends State<RoomCpInviteDialog> {
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 65.w,
|
||||
top: 74.w,
|
||||
child: _CountdownRow(text: _countdownText),
|
||||
),
|
||||
if (widget.actionState != RoomCpInviteActionState.accepted &&
|
||||
widget.actionState != RoomCpInviteActionState.rejected)
|
||||
Positioned(
|
||||
left: 65.w,
|
||||
top: 74.w,
|
||||
child: _CountdownRow(text: _countdownText),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -32,9 +32,11 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RtcProvider, SCRoomRocketStatusRes?>(
|
||||
selector: (_, provider) => provider.roomRocketStatus,
|
||||
builder: (context, status, child) {
|
||||
final provider = context.read<RtcProvider>();
|
||||
return AnimatedBuilder(
|
||||
animation: provider,
|
||||
builder: (context, child) {
|
||||
final status = provider.roomRocketStatus;
|
||||
if (status == null || !status.isEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@ -38,9 +38,11 @@ class RoomGiftComboFloatingLayer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RtcProvider, bool>(
|
||||
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
|
||||
builder: (context, showMic, child) {
|
||||
final provider = context.read<RtcProvider>();
|
||||
return AnimatedBuilder(
|
||||
animation: provider,
|
||||
builder: (context, child) {
|
||||
final showMic = _shouldShowRoomBottomMic(provider);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = _resolveRoomBottomInputWidth(
|
||||
@ -81,15 +83,16 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.read<RtcProvider>();
|
||||
return SizedBox(
|
||||
height: _floatingButtonHostHeight.w,
|
||||
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomBottomSnapshot(
|
||||
showMic: _shouldShowRoomBottomMic(provider),
|
||||
isMic: provider.isMic,
|
||||
),
|
||||
builder: (context, bottomSnapshot, child) {
|
||||
child: AnimatedBuilder(
|
||||
animation: provider,
|
||||
builder: (context, child) {
|
||||
final bottomSnapshot = _RoomBottomSnapshot(
|
||||
showMic: _shouldShowRoomBottomMic(provider),
|
||||
isMic: provider.isMic,
|
||||
);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = _resolveRoomBottomInputWidth(
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
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 {
|
||||
const RoomEmojiAssetImage({
|
||||
@ -20,12 +22,12 @@ class RoomEmojiAssetImage extends StatefulWidget {
|
||||
|
||||
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late AssetImage _imageProvider;
|
||||
AssetImage? _imageProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
_syncImageProvider();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -38,7 +40,7 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.asset != widget.asset) {
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
_syncImageProvider();
|
||||
_precache();
|
||||
}
|
||||
}
|
||||
@ -46,9 +48,42 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
@override
|
||||
Widget build(BuildContext 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(
|
||||
child: Image(
|
||||
image: _imageProvider,
|
||||
image: imageProvider,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
@ -59,7 +94,19 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -9,6 +9,8 @@ import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/services/audio/rtc_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/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
@ -34,34 +36,34 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
|
||||
_RoomEmojiCategory(
|
||||
id: "fluent",
|
||||
iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
assets: [
|
||||
"sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_02.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_03.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_04.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_05.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_06.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_07.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_08.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_09.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_10.webp",
|
||||
iconResource: "sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
items: [
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_01.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_02.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_03.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_04.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_05.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_06.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_07.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_08.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_09.webp"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/fluent_emoji_10.webp"),
|
||||
],
|
||||
),
|
||||
_RoomEmojiCategory(
|
||||
id: "monkey",
|
||||
iconAsset: "sc_images/room/emoji/monkey_1.gif",
|
||||
assets: [
|
||||
"sc_images/room/emoji/monkey_1.gif",
|
||||
"sc_images/room/emoji/monkey_2.gif",
|
||||
"sc_images/room/emoji/monkey_3.gif",
|
||||
"sc_images/room/emoji/monkey_4.gif",
|
||||
"sc_images/room/emoji/monkey_5.gif",
|
||||
"sc_images/room/emoji/monkey_6.gif",
|
||||
"sc_images/room/emoji/monkey_7.gif",
|
||||
"sc_images/room/emoji/monkey_8.gif",
|
||||
"sc_images/room/emoji/monkey_9.gif",
|
||||
"sc_images/room/emoji/monkey_10.gif",
|
||||
iconResource: "sc_images/room/emoji/monkey_1.gif",
|
||||
items: [
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_1.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_2.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_3.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_4.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_5.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_6.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_7.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_8.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_9.gif"),
|
||||
_RoomEmojiItem.local("sc_images/room/emoji/monkey_10.gif"),
|
||||
],
|
||||
),
|
||||
];
|
||||
@ -99,6 +101,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
super.didChangeDependencies();
|
||||
if (showEmoji) {
|
||||
_precacheRoomEmojis();
|
||||
_loadRemoteRoomEmojis();
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,7 +250,12 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
}
|
||||
|
||||
Widget _buildEmojiPanel() {
|
||||
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
|
||||
final categories = _resolveRoomEmojiCategories();
|
||||
final selectedIndex = _selectedEmojiCategoryIndex.clamp(
|
||||
0,
|
||||
categories.length - 1,
|
||||
);
|
||||
final selectedCategory = categories[selectedIndex];
|
||||
return Container(
|
||||
height: 220.w,
|
||||
decoration: BoxDecoration(
|
||||
@ -265,18 +273,18 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
crossAxisSpacing: 18.w,
|
||||
mainAxisSpacing: 12.w,
|
||||
),
|
||||
itemCount: selectedCategory.assets.length,
|
||||
itemCount: selectedCategory.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final emojiAsset = selectedCategory.assets[index];
|
||||
final emojiItem = selectedCategory.items[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
_sendRoomEmoji(emojiAsset);
|
||||
_sendRoomEmoji(emojiItem.sendResource);
|
||||
},
|
||||
child: Center(
|
||||
child: RoomEmojiAssetImage(
|
||||
key: ValueKey(emojiAsset),
|
||||
asset: emojiAsset,
|
||||
key: ValueKey(emojiItem.displayResource),
|
||||
asset: emojiItem.displayResource,
|
||||
width: 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(
|
||||
height: 50.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
|
||||
@ -303,11 +314,11 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _roomEmojiCategories.length,
|
||||
itemCount: categories.length,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 10.w),
|
||||
itemBuilder: (context, index) {
|
||||
final category = _roomEmojiCategories[index];
|
||||
final selected = index == _selectedEmojiCategoryIndex;
|
||||
final category = categories[index];
|
||||
final selected = index == selectedIndex;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
@ -340,7 +351,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
child: Center(
|
||||
child: RoomEmojiAssetImage(
|
||||
key: ValueKey("emoji_category_${category.id}"),
|
||||
asset: category.iconAsset,
|
||||
asset: category.iconResource,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
),
|
||||
@ -363,6 +374,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
|
||||
SCKeybordUtil.hide(context);
|
||||
_precacheRoomEmojis();
|
||||
_loadRemoteRoomEmojis();
|
||||
setState(() {
|
||||
showEmoji = true;
|
||||
});
|
||||
@ -374,13 +386,28 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
}
|
||||
_emojiAssetsPrecached = true;
|
||||
for (final category in _roomEmojiCategories) {
|
||||
precacheImage(AssetImage(category.iconAsset), context);
|
||||
for (final asset in category.assets) {
|
||||
precacheImage(AssetImage(asset), context);
|
||||
precacheImage(AssetImage(category.iconResource), context);
|
||||
for (final item in category.items) {
|
||||
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) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currenRoom = rtcProvider.currenRoom;
|
||||
@ -435,13 +462,54 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
class _RoomEmojiCategory {
|
||||
const _RoomEmojiCategory({
|
||||
required this.id,
|
||||
required this.iconAsset,
|
||||
required this.assets,
|
||||
required this.iconResource,
|
||||
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 iconAsset;
|
||||
final List<String> assets;
|
||||
final String iconResource;
|
||||
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 {
|
||||
|
||||
@ -408,8 +408,8 @@ class _MsgItemState extends State<MsgItem> {
|
||||
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
|
||||
final multilineMaxBubbleWidth = math.min(maxBubbleWidth, 300.w);
|
||||
final multilineMaxTextWidth = math.max(
|
||||
82.w,
|
||||
multilineMaxBubbleWidth - 64.w,
|
||||
60.w,
|
||||
multilineMaxBubbleWidth - 44.w,
|
||||
);
|
||||
final multilineTextPainter = TextPainter(
|
||||
text: TextSpan(text: messageText, style: textStyle),
|
||||
@ -443,15 +443,15 @@ class _MsgItemState extends State<MsgItem> {
|
||||
required double maxBubbleWidth,
|
||||
required double startMargin,
|
||||
}) {
|
||||
final leftPadding = 22.w;
|
||||
final rightPadding = 42.w;
|
||||
final topPadding = 16.w;
|
||||
final bottomPadding = 24.w;
|
||||
final leftPadding = 18.w;
|
||||
final rightPadding = 26.w;
|
||||
final topPadding = 10.w;
|
||||
final bottomPadding = 12.w;
|
||||
final bubbleWidth = math
|
||||
.max(126.w, textSize.width + leftPadding + rightPadding)
|
||||
.clamp(126.w, maxBubbleWidth);
|
||||
.max(92.w, textSize.width + leftPadding + rightPadding)
|
||||
.clamp(92.w, maxBubbleWidth);
|
||||
final textMaxWidth = math.max(
|
||||
64.w,
|
||||
48.w,
|
||||
bubbleWidth - leftPadding - rightPadding,
|
||||
);
|
||||
final textPainter = TextPainter(
|
||||
@ -460,7 +460,7 @@ class _MsgItemState extends State<MsgItem> {
|
||||
)..layout(maxWidth: textMaxWidth);
|
||||
final textBoxHeight = textPainter.size.height + 2.w;
|
||||
final bubbleHeight = math.max(
|
||||
76.w,
|
||||
52.w,
|
||||
textBoxHeight + topPadding + bottomPadding,
|
||||
);
|
||||
|
||||
@ -502,13 +502,13 @@ class _MsgItemState extends State<MsgItem> {
|
||||
required double maxBubbleWidth,
|
||||
required double startMargin,
|
||||
}) {
|
||||
final leftPadding = 22.w;
|
||||
final rightPadding = 24.w;
|
||||
final topPadding = 12.w;
|
||||
final bottomPadding = 16.w;
|
||||
final minSkinWidth = 96.w;
|
||||
final minSkinHeight = 58.w;
|
||||
final textWidthSafety = 10.w;
|
||||
final leftPadding = 16.w;
|
||||
final rightPadding = 18.w;
|
||||
final topPadding = 8.w;
|
||||
final bottomPadding = 10.w;
|
||||
final minSkinWidth = 56.w;
|
||||
final minSkinHeight = 52.w;
|
||||
final textWidthSafety = 4.w;
|
||||
final maxTextWidth = math.max(
|
||||
1.0,
|
||||
maxBubbleWidth - leftPadding - rightPadding,
|
||||
@ -1183,78 +1183,107 @@ class _MsgItemState extends State<MsgItem> {
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)),
|
||||
margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildUserInfoLine(context),
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
child: _buildGiftMessageItem(context),
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
Flexible(
|
||||
// 使用Flexible替代Expanded
|
||||
child: 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: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildUserNameAndLevels(user),
|
||||
_buildLongMedals(userId: user?.id, fallbackBadges: badges),
|
||||
_buildMedals(userId: user?.id, fallbackBadges: badges),
|
||||
SizedBox(height: 2.w),
|
||||
_buildGiftMessageBubble(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return SizedBox(width: width, height: height);
|
||||
}
|
||||
if (_isLocalAsset(path)) {
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
return netImage(url: path, width: width, height: height);
|
||||
}
|
||||
|
||||
bool _isLocalAsset(String path) {
|
||||
return path.startsWith("assets/") || path.startsWith("sc_images/");
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
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 |