feat: update home room and chat experience

This commit is contained in:
zhangxianbiao 2026-07-21 15:46:47 +08:00
parent d4a7633f28
commit 107b39b8a3
318 changed files with 82661 additions and 2883 deletions

View File

@ -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"

View File

@ -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))
}
}

View File

@ -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

View File

@ -129,6 +129,7 @@
"redEnvelopeNotYetClaimed": "المظروف الأحمر لم يُستلم بعد",
"redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات",
"followed": "تمت المتابعة",
"joined": "انضممت",
"newMessage": "رسالة جديدة",
"keep": "الاحتفاظ",
"open": "فتح",
@ -234,6 +235,7 @@
"deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟",
"account": "الحساب",
"youHaventFollowed": "أنت لم تتابع أي غرفة",
"youHaventJoinedAnyRoomsYet": "لم أنضم إلى أي غرف بعد.",
"messageHasBeenRecalled": "تم استرجاع هذه الرسالة",
"propMessagePrompt": "موجه رسالة دعائية",
"deleteFromMyDevice": "احذف من جهازي",
@ -560,6 +562,7 @@
"hotRooms": "الغرف الرائجة",
"viewMore": "عرض المزيد",
"noData": "لا توجد بيانات",
"noRoomRecordsRecently": "لا توجد سجلات غرف حديثة.",
"recommend": "موصى به",
"follow": "متابعة",
"monthly": "شهريًا",
@ -748,5 +751,8 @@
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
"launchRocket": "إطلاق الصاروخ",
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!"
"roomRocketFuelFullNotice": "اكتمل وقود الصاروخ! سيتم الإطلاق بعد {1} ثانية!",
"leaderboardWealth": "الثروة",
"leaderboardCharm": "الجاذبية",
"leaderboardRoom": "الغرفة"
}

View File

@ -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": "রুম"
}

View File

@ -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"
}

View File

@ -228,6 +228,7 @@
"youDontHaveAnyCouponsYet": "Vous n'avez pas encore de coupons.",
"recall": "Rappel",
"youHaventFollowed": "Vous n'avez suivi aucune pièce",
"youHaventJoinedAnyRoomsYet": "Je nai 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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -217,6 +217,7 @@
"youDontHaveAnyCouponsYet": "Henüz hiç kuponunuz yok.",
"recall": "Geri Çağır",
"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",
@ -356,6 +357,7 @@
"hotRooms": "Popüler Odalar",
"viewMore": "Daha Fazlasını Gör",
"noData": "Veri yok",
"noRoomRecordsRecently": "Yakın zamanda oda kaydı yok.",
"users": "Kullanıcılar",
"rooms": "Odalar",
"coins": "jetton",
@ -615,6 +617,7 @@
"purchaseIsSuccessful": "Satın alma başarılı",
"buy": "Satın Al",
"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
View 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

View 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 GatewayAPI_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 代码已落地、自动化验证通过、真实接口联调通过、不存在本地假数据兜底。

View File

@ -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

View File

@ -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";

View File

@ -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');

View File

@ -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,
],
),
),
);

View File

@ -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();

View File

@ -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}",

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -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 "";
}
///

View File

@ -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,
);
}
}

View 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;
}

View File

@ -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(

View File

@ -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;

View File

@ -211,6 +211,7 @@ class SCMainRoute implements SCIRouterProvider {
title: params['title']?.first ?? "",
url: decodedUrl,
showTitle: params['showTitle']?.first ?? "",
safeTop: params['safeTop']?.first == "true",
);
},
),

View File

@ -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),

View File

@ -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),

View File

@ -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),

View File

@ -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() {

View File

@ -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();

View File

@ -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),
],
),
),

View File

@ -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),
);

View File

@ -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),

View File

@ -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);

View File

@ -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();
}

View File

@ -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();
}
///

View File

@ -1,11 +1,11 @@
/// id : 0
/// sysOrigin : ""
/// groupCode : ""
/// groupName : ""
/// cover : ""
/// have : false
/// amount : 0.0
/// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
// id : 0
// sysOrigin : ""
// groupCode : ""
// groupName : ""
// cover : ""
// have : false
// amount : 0.0
// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
class SCRoomEmojiRes {
SCRoomEmojiRes({
@ -28,16 +28,17 @@ class SCRoomEmojiRes {
}
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) {
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 = [];
json['emojis'].forEach((v) {
map['emojis'].forEach((v) {
_emojis?.add(Emojis.fromJson(v));
});
}
@ -111,10 +112,11 @@ class Emojis {
}
Emojis.fromJson(dynamic json) {
_sourceUrl = json['sourceUrl'];
_sourceType = json['sourceType'];
_coverUrl = json['coverUrl'];
_type = json['type'];
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;
@ -144,3 +146,33 @@ Emojis copyWith({ String? sourceUrl,
}
}
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;
}

View File

@ -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,
),
);
}
}

View 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 '';
}

View 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');
}

View File

@ -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;

View File

@ -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;

View File

@ -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),
),
],
),
),

View File

@ -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();
}

View File

@ -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(

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View 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),
],
);
}

View 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

View File

@ -0,0 +1 @@
* @zmtzawqlp

View 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.

View File

@ -0,0 +1,616 @@
# extended_text_field
[![pub package](https://img.shields.io/pub/v/extended_text_field.svg)](https://pub.dartlang.org/packages/extended_text_field) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/extended_text_field)](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.
## 特殊文本
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field.gif)
### 创建特殊文本
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就可以了。
## 图片
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field_image.gif)
### 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
```
## 文本选择控制器
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/custom_toolbar.gif)
提供了默认的控制器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
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/widget_span.gif)
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
![img](http://zmtzawqlp.gitee.io/my_images/images/qrcode.png)

View File

@ -0,0 +1,605 @@
# extended_text_field
[![pub package](https://img.shields.io/pub/v/extended_text_field.svg)](https://pub.dartlang.org/packages/extended_text_field) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/extended_text_field)](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
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field.gif)
### 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
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field_image.gif)
### 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
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/custom_toolbar.gif)
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
![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/widget_span.gif)
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)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 {}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
package com.example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Some files were not shown because too many files have changed in this diff Show More