fix: Codex JsonNull 崩溃 + 5h 窗口优雅降级
- CodexRepository: 所有 ?.jsonObject/?.jsonPrimitive → as? JsonObject/as? JsonPrimitive 防止 API 返回 JSON null 时 IllegalArgumentException 闪退 - CodexRepository: parseUsageWindows() 外包 try/catch → ParseError 兜底 - CodexRepository: extras 新增 primary.label 供 UI 动态显示主窗口标签 - ServiceBalanceCards: CodexMainBalance 标签动态化 + CodexUsageWindows 保留 5h 空槽位 - ServiceDetailScreen: mainCardLabel 改为 "用量" + CodexUsageCard 保留 5h 空槽位 - Widget: Codex 窗口动态解析 + 5h 始终占第一行 - AGENTS.md: 新增 JsonNull 安全红线 + Codex 5h 降级说明
This commit is contained in:
parent
9d06644225
commit
f17d37ae39
@ -23,7 +23,7 @@ CommandCode Go 走 JSON API 抓取用量数据,Codex / ChatGPT Plus 通过 aut
|
|||||||
- ✅ DeepSeek — REST API `GET /user/balance`,API Key 认证
|
- ✅ DeepSeek — REST API `GET /user/balance`,API Key 认证
|
||||||
- ✅ OpenCode Go — OkHttp 抓 dashboard HTML,解析 `rollingUsage`/`weeklyUsage`/`monthlyUsage`
|
- ✅ OpenCode Go — OkHttp 抓 dashboard HTML,解析 `rollingUsage`/`weeklyUsage`/`monthlyUsage`
|
||||||
- ✅ CommandCode Go — JSON API 抓取用量数据,`CommandCodeUsageRepository` 解析(workspaceId = `"commandcode"`)
|
- ✅ CommandCode Go — JSON API 抓取用量数据,`CommandCodeUsageRepository` 解析(workspaceId = `"commandcode"`)
|
||||||
- ✅ Codex / ChatGPT Plus — 支持 **OAuth PKCE 登录(无头模式)** 或粘贴完整 auth.json(含 refresh_token),调 `chatgpt.com/backend-api/wham/usage`;token 过期前 60 分钟自动刷新。OpenAI 采用 refresh_token 单次轮换机制,被外部工具使用后旧 token 立即失效,需重新 OAuth 登录或导入新 auth.json。
|
- ✅ Codex / ChatGPT Plus — 支持 **OAuth PKCE 登录(无头模式)** 或粘贴完整 auth.json(含 refresh_token),调 `chatgpt.com/backend-api/wham/usage`;token 过期前 60 分钟自动刷新。OpenAI 采用 refresh_token 单次轮换机制,被外部工具使用后旧 token 立即失效,需重新 OAuth 登录或导入新 auth.json。**5h 窗口可能被 OpenAI 临时关闭**(`primary_window` 返回 `null`),UI 保留空 5h 槽位 + 主标签动态化(`extras["primary.label"]`),恢复后自动填回。
|
||||||
- ✅ Ollama Pro — Cookie 认证,OkHttp 抓 `ollama.com/settings` HTML,正则解析 plan/session(5h)/weekly 百分比 + `data-time` 重置时间 + `data-model` 模型级请求次数;无官方 API(ollama/ollama#12532)
|
- ✅ Ollama Pro — Cookie 认证,OkHttp 抓 `ollama.com/settings` HTML,正则解析 plan/session(5h)/weekly 百分比 + `data-time` 重置时间 + `data-model` 模型级请求次数;无官方 API(ollama/ollama#12532)
|
||||||
- ✅ 文案统一:所有服务标签均使用中文("每周"统一代替 "weekly"/"Weekly"/"weekly")
|
- ✅ 文案统一:所有服务标签均使用中文("每周"统一代替 "weekly"/"Weekly"/"weekly")
|
||||||
- ❌ OpenCode Zen / 小米 MiMo — 未实现
|
- ❌ OpenCode Zen / 小米 MiMo — 未实现
|
||||||
@ -41,6 +41,12 @@ CommandCode Go 走 JSON API 抓取用量数据,Codex / ChatGPT Plus 通过 aut
|
|||||||
- ✅ `DebugLog` — 内存 ring buffer(200 条,线程安全),所有 Repository 关键路径(网络请求、Token 刷新、解析错误)均写入日志;设置页提供「调试日志」入口,无需连电脑即可在 APP 内查看 ERROR/WARN/INFO 三级日志。
|
- ✅ `DebugLog` — 内存 ring buffer(200 条,线程安全),所有 Repository 关键路径(网络请求、Token 刷新、解析错误)均写入日志;设置页提供「调试日志」入口,无需连电脑即可在 APP 内查看 ERROR/WARN/INFO 三级日志。
|
||||||
- ✅ `RepositoryError.InvalidCredential` 支持自定义 `detail`,Codex `RefreshResult` 密封类明确区分刷新成功/失败原因,错误信息可透传到 Dashboard 卡片与调试日志。
|
- ✅ `RepositoryError.InvalidCredential` 支持自定义 `detail`,Codex `RefreshResult` 密封类明确区分刷新成功/失败原因,错误信息可透传到 Dashboard 卡片与调试日志。
|
||||||
|
|
||||||
|
**JsonNull 安全红线**:
|
||||||
|
|
||||||
|
> ⚠️ kotlinx.serialization 中 `?.jsonObject` / `?.jsonPrimitive` 对 JSON 显式 `null` 值无效——返回的是 `JsonNull` 对象(非 Kotlin null),扩展函数内部类型检查会抛 `IllegalArgumentException` 导致闪退。
|
||||||
|
> **必须用 `as? JsonObject` / `as? JsonPrimitive` 替代**,安全转换能正确处理 `JsonNull`(返回 null)。
|
||||||
|
> `parseUsageWindows()` 等解析方法外部必须包 `try/catch` → `RepositoryError.ParseError`,防止 API 响应结构变化导致异常逃逸。
|
||||||
|
|
||||||
**凭据回显红线**:
|
**凭据回显红线**:
|
||||||
|
|
||||||
> ⚠️ `CredentialEditViewModel.load()` 首次加载已有凭据时,需针对每种 `Credential` 子类显式编写回显分支。
|
> ⚠️ `CredentialEditViewModel.load()` 首次加载已有凭据时,需针对每种 `Credential` 子类显式编写回显分支。
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
import kotlinx.serialization.json.floatOrNull
|
import kotlinx.serialization.json.floatOrNull
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
@ -84,7 +86,12 @@ class CodexRepository(
|
|||||||
} else return@withContext Result.failure(e)
|
} else return@withContext Result.failure(e)
|
||||||
} catch (e: Throwable) { return@withContext Result.failure(RepositoryError.Unknown(e)) }
|
} catch (e: Throwable) { return@withContext Result.failure(RepositoryError.Unknown(e)) }
|
||||||
|
|
||||||
val windows = parseUsageWindows(usageResult)
|
val windows = try {
|
||||||
|
parseUsageWindows(usageResult)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DebugLog.e(TAG, "解析 Codex 用量失败: ${e::class.simpleName}: ${e.message}")
|
||||||
|
return@withContext Result.failure(RepositoryError.ParseError("Codex 用量响应格式异常: ${e.message ?: e::class.simpleName}"))
|
||||||
|
}
|
||||||
if (windows.isEmpty()) return@withContext Result.failure(RepositoryError.ParseError("未找到 Codex 用量窗口数据"))
|
if (windows.isEmpty()) return@withContext Result.failure(RepositoryError.ParseError("未找到 Codex 用量窗口数据"))
|
||||||
|
|
||||||
val config = ServiceConfigProvider.get(ServiceType.CODEX)
|
val config = ServiceConfigProvider.get(ServiceType.CODEX)
|
||||||
@ -95,9 +102,10 @@ class CodexRepository(
|
|||||||
put("window_$i.remainingPct", w.remainingPct.toString())
|
put("window_$i.remainingPct", w.remainingPct.toString())
|
||||||
put("window_$i.resetAt", w.resetAt?.toString() ?: "")
|
put("window_$i.resetAt", w.resetAt?.toString() ?: "")
|
||||||
}
|
}
|
||||||
usageResult["plan_type"]?.jsonPrimitive?.content?.let { put("plan", it) }
|
put("primary.label", primary.label)
|
||||||
usageResult["credits"]?.jsonObject?.let { c ->
|
(usageResult["plan_type"] as? JsonPrimitive)?.contentOrNull?.let { put("plan", it) }
|
||||||
c["balance"]?.jsonPrimitive?.floatOrNull?.let { put("usageCredits", it.toString()) }
|
(usageResult["credits"] as? JsonObject)?.let { c ->
|
||||||
|
(c["balance"] as? JsonPrimitive)?.floatOrNull?.let { put("usageCredits", it.toString()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val balance = ServiceBalance(ServiceType.CODEX, (100 - primary.remainingPct).coerceIn(0, 100).toDouble(), config.displayUnit, true, null, null, primary.resetAt, extras)
|
val balance = ServiceBalance(ServiceType.CODEX, (100 - primary.remainingPct).coerceIn(0, 100).toDouble(), config.displayUnit, true, null, null, primary.resetAt, extras)
|
||||||
@ -154,7 +162,11 @@ class CodexRepository(
|
|||||||
if (errorBody == null) return "HTTP $httpCode,无错误详情"
|
if (errorBody == null) return "HTTP $httpCode,无错误详情"
|
||||||
// 尝试提取 error.message 字段
|
// 尝试提取 error.message 字段
|
||||||
val msg = try {
|
val msg = try {
|
||||||
json.parseToJsonElement(errorBody).jsonObject["error"]?.jsonObject?.get("message")?.jsonPrimitive?.content
|
(json.parseToJsonElement(errorBody) as? JsonObject)?.let { root ->
|
||||||
|
(root["error"] as? JsonObject)?.let { err ->
|
||||||
|
(err["message"] as? JsonPrimitive)?.contentOrNull
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
return when {
|
return when {
|
||||||
msg != null && msg.contains("already been used") ->
|
msg != null && msg.contains("already been used") ->
|
||||||
@ -183,7 +195,8 @@ class CodexRepository(
|
|||||||
DebugLog.e(TAG, "fetchJson failed: HTTP ${it.code} ${it.message} url=$url")
|
DebugLog.e(TAG, "fetchJson failed: HTTP ${it.code} ${it.message} url=$url")
|
||||||
throw if (it.code in listOf(401, 403)) RepositoryError.InvalidCredential("HTTP ${it.code}") else RepositoryError.ServerError(it.code)
|
throw if (it.code in listOf(401, 403)) RepositoryError.InvalidCredential("HTTP ${it.code}") else RepositoryError.ServerError(it.code)
|
||||||
}
|
}
|
||||||
return json.parseToJsonElement(it.body?.string() ?: throw RepositoryError.ParseError("响应体为空")).jsonObject
|
return (json.parseToJsonElement(it.body?.string() ?: throw RepositoryError.ParseError("响应体为空")) as? JsonObject)
|
||||||
|
?: throw RepositoryError.ParseError("响应根节点不是 JSON 对象")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,15 +207,15 @@ class CodexRepository(
|
|||||||
fun addWindows(rl: JsonObject?) {
|
fun addWindows(rl: JsonObject?) {
|
||||||
if (rl == null) return
|
if (rl == null) return
|
||||||
for (key in listOf("primary_window", "secondary_window")) {
|
for (key in listOf("primary_window", "secondary_window")) {
|
||||||
val w = rl[key]?.jsonObject ?: continue
|
val w = rl[key] as? JsonObject ?: continue
|
||||||
val usedPct = w["used_percent"]?.jsonPrimitive?.floatOrNull ?: continue
|
val usedPct = (w["used_percent"] as? JsonPrimitive)?.floatOrNull ?: continue
|
||||||
val remaining = (100 - usedPct).toInt().coerceIn(0, 100)
|
val remaining = (100 - usedPct).toInt().coerceIn(0, 100)
|
||||||
result.add(UsageWindow(durationLabel(w["limit_window_seconds"]?.jsonPrimitive?.longOrNull), remaining, w["reset_at"]?.jsonPrimitive?.longOrNull?.times(1000L)))
|
result.add(UsageWindow(durationLabel((w["limit_window_seconds"] as? JsonPrimitive)?.longOrNull), remaining, (w["reset_at"] as? JsonPrimitive)?.longOrNull?.times(1000L)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addWindows(data["rate_limit"]?.jsonObject)
|
addWindows(data["rate_limit"] as? JsonObject)
|
||||||
(data["additional_rate_limits"] as? kotlinx.serialization.json.JsonArray)?.forEach { item ->
|
(data["additional_rate_limits"] as? kotlinx.serialization.json.JsonArray)?.forEach { item ->
|
||||||
if (item is JsonObject) addWindows(item["rate_limit"]?.jsonObject)
|
if (item is JsonObject) addWindows(item["rate_limit"] as? JsonObject)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -226,6 +226,7 @@ internal fun CodexMainBalance(balance: ServiceBalance) {
|
|||||||
val plan = balance.extras["plan"]?.let {
|
val plan = balance.extras["plan"]?.let {
|
||||||
when (it) { "plus" -> "Plus"; "pro" -> "Pro"; "free" -> "Free"; else -> it.replaceFirstChar { c -> c.uppercaseChar() } }
|
when (it) { "plus" -> "Plus"; "pro" -> "Pro"; "free" -> "Free"; else -> it.replaceFirstChar { c -> c.uppercaseChar() } }
|
||||||
} ?: "—"
|
} ?: "—"
|
||||||
|
val primaryLabel = formatCodexPrimaryLabel(balance.extras["primary.label"])
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text(
|
Text(
|
||||||
text = formatAmount(balance.amount),
|
text = formatAmount(balance.amount),
|
||||||
@ -242,7 +243,7 @@ internal fun CodexMainBalance(balance: ServiceBalance) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "5h 已用",
|
text = "$primaryLabel 已用",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = inkMuted(),
|
color = inkMuted(),
|
||||||
modifier = Modifier.padding(bottom = 6.dp)
|
modifier = Modifier.padding(bottom = 6.dp)
|
||||||
@ -257,6 +258,16 @@ internal fun CodexMainBalance(balance: ServiceBalance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 把 Codex API 返回的 primary label 转成中文短标签 */
|
||||||
|
internal fun formatCodexPrimaryLabel(raw: String?): String = when (raw?.lowercase()) {
|
||||||
|
"5h" -> "5h"
|
||||||
|
"7d", "每周" -> "每周"
|
||||||
|
"30d", "每月" -> "每月"
|
||||||
|
"usage" -> "用量"
|
||||||
|
null -> "5h"
|
||||||
|
else -> raw
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CodexUsageWindows(balance: ServiceBalance) {
|
internal fun CodexUsageWindows(balance: ServiceBalance) {
|
||||||
val extras = balance.extras
|
val extras = balance.extras
|
||||||
@ -272,10 +283,17 @@ internal fun CodexUsageWindows(balance: ServiceBalance) {
|
|||||||
Triple(label, remainingPct, resetAt)
|
Triple(label, remainingPct, resetAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断是否有 5h 窗口(用于保留空槽位)
|
||||||
|
val has5h = windows.any { it.first.contains("5") && it.first.contains("小时") }
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
if (windows.isEmpty()) {
|
if (windows.isEmpty()) {
|
||||||
CompactUsageRowEmpty(label = "Usage", resetInSec = null)
|
CompactUsageRowEmpty(label = "Usage", resetInSec = null)
|
||||||
} else {
|
} else {
|
||||||
|
// 如果没有 5h 窗口,在顶部插入一个空的 5h 槽位(保留位置,等恢复后自动填充)
|
||||||
|
if (!has5h) {
|
||||||
|
CompactUsageRowEmpty(label = "5 小时", resetInSec = null)
|
||||||
|
}
|
||||||
windows.forEach { (label, remainingPct, resetAt) ->
|
windows.forEach { (label, remainingPct, resetAt) ->
|
||||||
if (remainingPct != null) {
|
if (remainingPct != null) {
|
||||||
val usedPct = (100 - remainingPct).coerceIn(0, 100)
|
val usedPct = (100 - remainingPct).coerceIn(0, 100)
|
||||||
|
|||||||
@ -410,6 +410,9 @@ private fun CodexUsageCard(state: State) {
|
|||||||
val windows = remember(extras) { extractCodexWindows(extras) }
|
val windows = remember(extras) { extractCodexWindows(extras) }
|
||||||
if (windows.isEmpty()) return
|
if (windows.isEmpty()) return
|
||||||
|
|
||||||
|
// 判断是否有 5h 窗口
|
||||||
|
val has5h = windows.any { it.label.contains("5") && it.label.contains("小时") }
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@ -434,6 +437,20 @@ private fun CodexUsageCard(state: State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 没有 5h 窗口时,在顶部插入空槽位
|
||||||
|
if (!has5h) {
|
||||||
|
UsageWindowRow(
|
||||||
|
label = "5 小时",
|
||||||
|
pct = null,
|
||||||
|
resetInSec = null,
|
||||||
|
decimals = 2
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
|
||||||
windows.forEachIndexed { index, window ->
|
windows.forEachIndexed { index, window ->
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
Spacer(modifier = Modifier.height(14.dp))
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
@ -850,7 +867,7 @@ private fun mainCardLabel(service: ServiceType): String = when (service) {
|
|||||||
ServiceType.DEEPSEEK -> "当前余额"
|
ServiceType.DEEPSEEK -> "当前余额"
|
||||||
ServiceType.OPENCODE_GO -> "5h 实时用量"
|
ServiceType.OPENCODE_GO -> "5h 实时用量"
|
||||||
ServiceType.COMMANDCODE_GO -> "月度余额"
|
ServiceType.COMMANDCODE_GO -> "月度余额"
|
||||||
ServiceType.CODEX -> "5h 已用量"
|
ServiceType.CODEX -> "用量"
|
||||||
ServiceType.OLLAMA -> "Session 用量"
|
ServiceType.OLLAMA -> "Session 用量"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -177,20 +177,34 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
|
|||||||
resetSec = extras["monthly.resetInSec"]?.toLongOrNull())
|
resetSec = extras["monthly.resetInSec"]?.toLongOrNull())
|
||||||
}
|
}
|
||||||
ServiceType.CODEX -> {
|
ServiceType.CODEX -> {
|
||||||
val windows = (0 until 3).map { index ->
|
val windowCount = extras.keys
|
||||||
val label = normalizeWindowLabel(extras["window_$index.label"] ?: if (index == 0) "5h" else "Usage")
|
.mapNotNull { key -> key.removePrefix("window_").substringBefore('.').toIntOrNull() }
|
||||||
|
.distinct().maxOrNull()?.plus(1) ?: 0
|
||||||
|
val windows = (0 until windowCount).map { index ->
|
||||||
|
val label = normalizeWindowLabel(extras["window_$index.label"] ?: "Usage")
|
||||||
val remaining = extras["window_$index.remainingPct"]?.toIntOrNull()
|
val remaining = extras["window_$index.remainingPct"]?.toIntOrNull()
|
||||||
val resetAt = extras["window_$index.resetAt"]?.toLongOrNull()?.takeIf { it > 0 }
|
val resetAt = extras["window_$index.resetAt"]?.toLongOrNull()?.takeIf { it > 0 }
|
||||||
Triple(label, remaining?.let { (100 - it).coerceIn(0, 100) }, resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 })
|
Triple(label, remaining?.let { (100 - it).coerceIn(0, 100) }, resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 })
|
||||||
}
|
}
|
||||||
setRowLabel(views, windows.getOrNull(0)?.first ?: "5h", windows.getOrNull(1)?.first ?: "周", windows.getOrNull(2)?.first ?: "月")
|
// 判断是否有 5h 窗口
|
||||||
|
val has5h = windows.any { it.first.contains("5") }
|
||||||
|
// 始终保留 5h 槽位在第一行
|
||||||
|
val row1 = if (!has5h) {
|
||||||
|
Triple("5h", null, null)
|
||||||
|
} else {
|
||||||
|
windows.firstOrNull { it.first.contains("5") } ?: windows.getOrNull(0) ?: Triple("5h", null, null)
|
||||||
|
}
|
||||||
|
val otherWindows = windows.filter { it != row1 }
|
||||||
|
val row2 = otherWindows.getOrNull(0) ?: Triple("每周", null, null)
|
||||||
|
val row3 = otherWindows.getOrNull(1) ?: Triple("每月", null, null)
|
||||||
|
setRowLabel(views, row1.first, row2.first, row3.first)
|
||||||
listOf(
|
listOf(
|
||||||
Triple(R.id.row1_pct, R.id.row1_bar, R.id.row1_reset),
|
Triple(R.id.row1_pct, R.id.row1_bar, R.id.row1_reset),
|
||||||
Triple(R.id.row2_pct, R.id.row2_bar, R.id.row2_reset),
|
Triple(R.id.row2_pct, R.id.row2_bar, R.id.row2_reset),
|
||||||
Triple(R.id.row3_pct, R.id.row3_bar, R.id.row3_reset)
|
Triple(R.id.row3_pct, R.id.row3_bar, R.id.row3_reset)
|
||||||
).forEachIndexed { index, ids ->
|
).forEachIndexed { index, ids ->
|
||||||
val window = windows.getOrNull(index)
|
val window = when (index) { 0 -> row1; 1 -> row2; else -> row3 }
|
||||||
populateRow(views, ids.first, ids.second, ids.third, pct = window?.second, resetSec = window?.third)
|
populateRow(views, ids.first, ids.second, ids.third, pct = window.second, resetSec = window.third)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServiceType.DEEPSEEK -> setEmptyState(views)
|
ServiceType.DEEPSEEK -> setEmptyState(views)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user