feat(ui): Codex 主模型余额为主视觉,Spark 弱化为次要摘要

- 首页卡片:主模型窗口独占进度行,Spark 收为单行小字摘要\n- 详情页:Spark 收进分隔线 + 小标题次要小节,细条低饱和进度\n- 桌面小组件:跳过 SPARK 窗口,仅保留主模型三行布局\n- 新增 window_spark 多语言文案
This commit is contained in:
WaterRain 2026-08-24 08:49:46 +00:00
parent 7e44e545bf
commit 43d8df90f6
No known key found for this signature in database
6 changed files with 158 additions and 39 deletions

View File

@ -297,21 +297,34 @@ internal fun CodexUsageWindows(balance: ServiceBalance) {
val weeklyLabel = stringResource(R.string.window_every_week) val weeklyLabel = stringResource(R.string.window_every_week)
val monthlyLabel = stringResource(R.string.window_every_month) val monthlyLabel = stringResource(R.string.window_every_month)
val usageLabel = stringResource(R.string.window_usage)
val sparkTitle = stringResource(R.string.window_spark)
data class UiWindow(val label: String, val isSpark: Boolean, val remainingPct: Int?, val resetAt: Long?)
val windows = (0 until windowCount).map { i -> val windows = (0 until windowCount).map { i ->
val rawLabel = extras["window_$i.label"] ?: "usage"
val label = normalizeWindowLabel( val label = normalizeWindowLabel(
extras["window_$i.label"] ?: "usage", rawLabel,
weeklyLabel = weeklyLabel, weeklyLabel = weeklyLabel,
monthlyLabel = monthlyLabel, monthlyLabel = monthlyLabel,
usageLabel = stringResource(R.string.window_usage) usageLabel = usageLabel
)
UiWindow(
label = label,
isSpark = extras["window_$i.group"] == "SPARK",
remainingPct = extras["window_$i.remainingPct"]?.toIntOrNull(),
resetAt = extras["window_$i.resetAt"]?.toLongOrNull()?.takeIf { it > 0 }
) )
val remainingPct = extras["window_$i.remainingPct"]?.toIntOrNull()
val resetAt = extras["window_$i.resetAt"]?.toLongOrNull()?.takeIf { it > 0 }
Triple(label, remainingPct, resetAt)
} }
// 判断是否有 5h 窗口(与语言无关:匹配 "5h"/"5H" 或本地化标签,用于保留空槽位) // 主模型(普通 Codex)窗口独占全部进度行;Spark 独立限额是次要通道,只留一行小字摘要
val mainWindows = windows.filter { !it.isSpark }
val sparkWindows = windows.filter { it.isSpark }
// 判断是否有 5h 窗口(与语言无关:匹配 "5h"/"5H" 或本地化标签,仅看主模型窗口)
val fiveHourLabel = stringResource(R.string.window_5h) val fiveHourLabel = stringResource(R.string.window_5h)
val has5h = windows.any { isFiveHourLabel(it.first, fiveHourLabel, stringResource(R.string.window_5h_short)) } val has5h = mainWindows.any { isFiveHourLabel(it.label, fiveHourLabel, stringResource(R.string.window_5h_short)) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (windows.isEmpty()) { if (windows.isEmpty()) {
@ -321,14 +334,26 @@ internal fun CodexUsageWindows(balance: ServiceBalance) {
if (!has5h) { if (!has5h) {
CompactUsageRowEmpty(label = fiveHourLabel, resetInSec = null) CompactUsageRowEmpty(label = fiveHourLabel, resetInSec = null)
} }
windows.forEach { (label, remainingPct, resetAt) -> mainWindows.forEach { w ->
if (remainingPct != null) { val resetSec = w.resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 }
val usedPct = (100 - remainingPct).coerceIn(0, 100) if (w.remainingPct != null) {
CompactUsageRow(label = label, pct = usedPct, resetInSec = resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 }) CompactUsageRow(label = w.label, pct = (100 - w.remainingPct).coerceIn(0, 100), resetInSec = resetSec)
} else { } else {
CompactUsageRowEmpty(label = label, resetInSec = resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 }) CompactUsageRowEmpty(label = w.label, resetInSec = resetSec)
} }
} }
// Spark 次要限额:单行摘要,不与主模型争夺视觉焦点
if (sparkWindows.isNotEmpty()) {
val summary = sparkWindows.joinToString(" · ") { w ->
val used = w.remainingPct?.let { (100 - it).coerceIn(0, 100) }
if (used != null) "${w.label} $used%" else "${w.label} —"
}
Text(
text = "$sparkTitle · $summary",
style = MaterialTheme.typography.bodySmall,
color = inkMuted()
)
}
} }
} }
} }

View File

@ -485,10 +485,13 @@ private fun CodexUsageCard(state: State) {
} }
val extras = balance?.extras ?: return val extras = balance?.extras ?: return
val plan = extras["plan"].orEmpty() val plan = extras["plan"].orEmpty()
val windows = remember(extras) { extractCodexWindows(extras) } val allWindows = remember(extras) { extractCodexWindows(extras) }
if (windows.isEmpty()) return if (allWindows.isEmpty()) return
// 主模型窗口占据主视觉;Spark 独立限额收进底部次要小节
val windows = allWindows.filter { it.group == CodexWindowGroup.CODEX }
val sparkWindows = allWindows.filter { it.group == CodexWindowGroup.SPARK }
// 判断是否有 5h 窗口(与语言无关:匹配 "5h"/"5H" 或本地化标签) // 判断是否有 5h 窗口(与语言无关:匹配 "5h"/"5H" 或本地化标签;仅看主模型窗口)
val has5h = windows.any { val has5h = windows.any {
isFiveHourLabel(it.rawLabel, stringResource(R.string.window_5h), stringResource(R.string.window_5h_short)) isFiveHourLabel(it.rawLabel, stringResource(R.string.window_5h), stringResource(R.string.window_5h_short))
} }
@ -547,14 +550,100 @@ private fun CodexUsageCard(state: State) {
decimals = 2 decimals = 2
) )
} }
// Spark 独立限额(次要通道):分隔线 + 小标题 + 紧凑细条,视觉权重远低于主模型
if (sparkWindows.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.window_spark),
style = MaterialTheme.typography.labelMedium,
color = inkMuted()
)
Spacer(modifier = Modifier.height(8.dp))
sparkWindows.forEach { window ->
val resetInSec = window.resetAt?.let {
(it - System.currentTimeMillis()) / 1000
}?.takeIf { it > 0 }
SparkUsageRow(
label = codexWindowLabel(window.rawLabel),
pct = window.usedPct,
resetInSec = resetInSec
)
Spacer(modifier = Modifier.height(8.dp))
}
}
} }
} }
} }
/**
* Spark 次要限额的紧凑行:小字号 + 细进度条,视觉权重明显低于主模型的 [UsageWindowRow]。
*/
@Composable
private fun SparkUsageRow(label: String, pct: Float, resetInSec: Long?) {
val durationText = DurationText(
day = stringResource(R.string.format_day),
hour = stringResource(R.string.format_hour),
minute = stringResource(R.string.format_minute)
)
val pctValue = pct.coerceIn(0f, 100f)
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
Text(
text = String.format(Locale.US, "%.1f", pctValue),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "%",
style = MaterialTheme.typography.labelSmall,
color = inkMuted(),
modifier = Modifier.padding(start = 1.dp)
)
}
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = { pctValue / 100f },
modifier = Modifier
.fillMaxWidth()
.height(2.5.dp)
.clip(RoundedCornerShape(1.25.dp)),
color = when {
pctValue >= 80f -> MaterialTheme.colorScheme.error.copy(alpha = 0.75f)
else -> StrawberryPink.copy(alpha = 0.55f)
},
trackColor = MaterialTheme.colorScheme.surfaceVariant
)
if (resetInSec != null && resetInSec > 0) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.service_reset_in, formatResetInSec(resetInSec, durationText)),
style = MaterialTheme.typography.labelSmall,
color = inkMuted()
)
}
}
}
private enum class CodexWindowGroup { CODEX, SPARK }
private data class CodexWindow( private data class CodexWindow(
val rawLabel: String, val rawLabel: String,
val usedPct: Float, val usedPct: Float,
val resetAt: Long? val resetAt: Long?,
val group: CodexWindowGroup = CodexWindowGroup.CODEX
) )
private fun extractCodexWindows(extras: Map<String, String>): List<CodexWindow> { private fun extractCodexWindows(extras: Map<String, String>): List<CodexWindow> {
@ -565,7 +654,8 @@ private fun extractCodexWindows(extras: Map<String, String>): List<CodexWindow>
val remaining = extras["window_${i}.remainingPct"]?.toFloatOrNull() ?: 0f val remaining = extras["window_${i}.remainingPct"]?.toFloatOrNull() ?: 0f
val resetAt = extras["window_${i}.resetAt"]?.toLongOrNull()?.takeIf { it > 0 } val resetAt = extras["window_${i}.resetAt"]?.toLongOrNull()?.takeIf { it > 0 }
val usedPct = (100f - remaining).coerceIn(0f, 100f) val usedPct = (100f - remaining).coerceIn(0f, 100f)
result.add(CodexWindow(rawLabel, usedPct, resetAt)) val group = if (extras["window_${i}.group"] == "SPARK") CodexWindowGroup.SPARK else CodexWindowGroup.CODEX
result.add(CodexWindow(rawLabel, usedPct, resetAt, group))
i++ i++
} }
return result return result
@ -574,14 +664,14 @@ private fun extractCodexWindows(extras: Map<String, String>): List<CodexWindow>
@Composable @Composable
private fun codexWindowLabel(raw: String): String = when (raw.lowercase()) { private fun codexWindowLabel(raw: String): String = when (raw.lowercase()) {
"5h" -> stringResource(R.string.window_5h) "5h" -> stringResource(R.string.window_5h)
"7d", "weekly", "每周" -> stringResource(R.string.window_weekly) "7d", "weekly", "每周", "每週" -> stringResource(R.string.window_weekly)
"30d", "monthly", "每月" -> stringResource(R.string.window_monthly) "30d", "monthly", "每月" -> stringResource(R.string.window_monthly)
"usage" -> stringResource(R.string.window_usage) "usage" -> stringResource(R.string.window_usage)
else -> raw else -> raw
} }
/** /**
* Ollama Pro 专属:5h + 每周用量窗口卡 + 模型级调用次数。 * Ollama 专属:5h + 每周用量窗口卡 + 模型级调用次数。
*/ */
@Composable @Composable
private fun OllamaUsageCard(state: State) { private fun OllamaUsageCard(state: State) {

View File

@ -205,26 +205,27 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
val weeklyLabel = context.getString(R.string.window_every_week) val weeklyLabel = context.getString(R.string.window_every_week)
val monthlyLabel = context.getString(R.string.window_every_month) val monthlyLabel = context.getString(R.string.window_every_month)
val usageLabel = context.getString(R.string.window_usage) val usageLabel = context.getString(R.string.window_usage)
val windows = (0 until windowCount).map { index -> // 仅主模型窗口进入桌面组件;Spark 独立限额为次要通道,不占用组件行位
val normal = mutableListOf<Triple<String, Int?, Long?>>()
for (index in 0 until windowCount) {
if (extras["window_$index.group"] == "SPARK") continue
val rawLabel = extras["window_$index.label"] ?: "usage"
val label = normalizeWindowLabel( val label = normalizeWindowLabel(
extras["window_$index.label"] ?: "usage", rawLabel,
weeklyLabel = weeklyLabel, weeklyLabel = weeklyLabel,
monthlyLabel = monthlyLabel, monthlyLabel = monthlyLabel,
usageLabel = usageLabel usageLabel = usageLabel
) )
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 }) normal.add(Triple(label, remaining?.let { (100 - it).coerceIn(0, 100) }, resetAt?.let { (it - System.currentTimeMillis()) / 1000 }?.takeIf { it > 0 }))
} }
// 判断是否有 5h 窗口 // 判断是否有 5h 窗口
val has5h = windows.any { it.first.contains("5") } val has5h = normal.any { it.first.contains("5") }
// 始终保留 5h 槽位在第一行 // 始终保留 5h 槽位在第一行(按索引取行,避免内容相同的窗口被误过滤)
val row1 = if (!has5h) { val row1Idx = if (has5h) (normal.indexOfFirst { it.first.contains("5") }.takeIf { it >= 0 } ?: 0) else -1
Triple("5h", null, null) val row1 = if (row1Idx >= 0) normal[row1Idx] else Triple("5h", null, null)
} else { val otherWindows = normal.filterIndexed { idx, _ -> idx != row1Idx }
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(weeklyLabel, null, null) val row2 = otherWindows.getOrNull(0) ?: Triple(weeklyLabel, null, null)
val row3 = otherWindows.getOrNull(1) ?: Triple(monthlyLabel, null, null) val row3 = otherWindows.getOrNull(1) ?: Triple(monthlyLabel, null, null)
setRowLabel(views, row1.first, row2.first, row3.first) setRowLabel(views, row1.first, row2.first, row3.first)

View File

@ -106,7 +106,7 @@
<string name="service_desc_deepseek">REST API · ¥</string> <string name="service_desc_deepseek">REST API · ¥</string>
<string name="service_desc_opencode_go">WebView 抓取 · 5h 配额</string> <string name="service_desc_opencode_go">WebView 抓取 · 5h 配额</string>
<string name="service_desc_commandcode_go">JSON API · $</string> <string name="service_desc_commandcode_go">JSON API · $</string>
<string name="service_desc_codex">ChatGPT Plus · Codex 额度</string> <string name="service_desc_codex">ChatGPT · Codex 额度</string>
<string name="service_desc_ollama">Cookie 抓取 · Cloud 配额</string> <string name="service_desc_ollama">Cookie 抓取 · Cloud 配额</string>
<!-- ══════════ 时长标签(DurationText 注入用) ══════════ --> <!-- ══════════ 时长标签(DurationText 注入用) ══════════ -->
@ -385,7 +385,7 @@
<string name="help_codex_12"> }}</string> <string name="help_codex_12"> }}</string>
<string name="help_codex_note">提示:导入后 APP 会自动刷新 token,</string> <string name="help_codex_note">提示:导入后 APP 会自动刷新 token,</string>
<string name="help_codex_note2">不需要手动更新。</string> <string name="help_codex_note2">不需要手动更新。</string>
<string name="credential_title_ollama">Ollama Pro 凭据</string> <string name="credential_title_ollama">Ollama 凭据</string>
<string name="credential_hint_ollama">需要从浏览器复制完整 Cookie。登录 ollama.com/settings 后,在 DevTools → Network → 请求头里找到 Cookie 行复制。</string> <string name="credential_hint_ollama">需要从浏览器复制完整 Cookie。登录 ollama.com/settings 后,在 DevTools → Network → 请求头里找到 Cookie 行复制。</string>
<string name="credential_update">更新凭据</string> <string name="credential_update">更新凭据</string>
<string name="credential_save">保存凭据</string> <string name="credential_save">保存凭据</string>
@ -414,7 +414,7 @@
<string name="api_key_form_title">请输入 API Key</string> <string name="api_key_form_title">请输入 API Key</string>
<string name="api_key_form_hint">凭据会使用 Android Keystore (AES-256 GCM) 加密后存储到本机 DataStore。</string> <string name="api_key_form_hint">凭据会使用 Android Keystore (AES-256 GCM) 加密后存储到本机 DataStore。</string>
<string name="action_update">更新</string> <string name="action_update">更新</string>
<string name="credential_title_codex">Codex / ChatGPT Plus 凭据</string> <string name="credential_title_codex">Codex / ChatGPT 凭据</string>
<string name="credential_hint_codex">推荐使用 OAuth 登录自动获取凭据。也可手动粘贴 auth.json 内容。</string> <string name="credential_hint_codex">推荐使用 OAuth 登录自动获取凭据。也可手动粘贴 auth.json 内容。</string>
<string name="action_oauth_login">🔐 OAuth 登录(推荐)</string> <string name="action_oauth_login">🔐 OAuth 登录(推荐)</string>
<string name="or_manual_import">── 或手动导入 ──</string> <string name="or_manual_import">── 或手动导入 ──</string>
@ -435,6 +435,7 @@
<string name="error_parse_models_empty">模型列表为空</string> <string name="error_parse_models_empty">模型列表为空</string>
<string name="error_parse_malformed">响应格式异常</string> <string name="error_parse_malformed">响应格式异常</string>
<string name="window_every_week">每周</string> <string name="window_every_week">每周</string>
<string name="window_spark">Spark</string>
<string name="msg_model_call_count">模型调用次数</string> <string name="msg_model_call_count">模型调用次数</string>
<string name="msg_stale_cache">缓存数据 · %1$s</string> <string name="msg_stale_cache">缓存数据 · %1$s</string>
<string name="msg_showing_last_balance">显示的是上次成功获取的余额</string> <string name="msg_showing_last_balance">显示的是上次成功获取的余额</string>

View File

@ -106,7 +106,7 @@
<string name="service_desc_deepseek">REST API · ¥</string> <string name="service_desc_deepseek">REST API · ¥</string>
<string name="service_desc_opencode_go">WebView 抓取 · 5h 配額</string> <string name="service_desc_opencode_go">WebView 抓取 · 5h 配額</string>
<string name="service_desc_commandcode_go">JSON API · $</string> <string name="service_desc_commandcode_go">JSON API · $</string>
<string name="service_desc_codex">ChatGPT Plus · Codex 額度</string> <string name="service_desc_codex">ChatGPT · Codex 額度</string>
<string name="service_desc_ollama">Cookie 抓取 · Cloud 配額</string> <string name="service_desc_ollama">Cookie 抓取 · Cloud 配額</string>
<!-- ══════════ 时长标签(DurationText 注入用) ══════════ --> <!-- ══════════ 时长标签(DurationText 注入用) ══════════ -->
@ -385,7 +385,7 @@
<string name="help_codex_12"> }}</string> <string name="help_codex_12"> }}</string>
<string name="help_codex_note">提示:匯入後 APP 會自動重新整理 token,</string> <string name="help_codex_note">提示:匯入後 APP 會自動重新整理 token,</string>
<string name="help_codex_note2">不需要手動更新。</string> <string name="help_codex_note2">不需要手動更新。</string>
<string name="credential_title_ollama">Ollama Pro 憑據</string> <string name="credential_title_ollama">Ollama 憑據</string>
<string name="credential_hint_ollama">需要從瀏覽器複製完整 Cookie。登入 ollama.com/settings 後,在 DevTools → Network → 請求頭裡找到 Cookie 行複製。</string> <string name="credential_hint_ollama">需要從瀏覽器複製完整 Cookie。登入 ollama.com/settings 後,在 DevTools → Network → 請求頭裡找到 Cookie 行複製。</string>
<string name="credential_update">更新憑據</string> <string name="credential_update">更新憑據</string>
<string name="credential_save">儲存憑據</string> <string name="credential_save">儲存憑據</string>
@ -414,7 +414,7 @@
<string name="api_key_form_title">請輸入 API Key</string> <string name="api_key_form_title">請輸入 API Key</string>
<string name="api_key_form_hint">憑據會使用 Android Keystore (AES-256 GCM) 加密後儲存到本機 DataStore。</string> <string name="api_key_form_hint">憑據會使用 Android Keystore (AES-256 GCM) 加密後儲存到本機 DataStore。</string>
<string name="action_update">更新</string> <string name="action_update">更新</string>
<string name="credential_title_codex">Codex / ChatGPT Plus 憑據</string> <string name="credential_title_codex">Codex / ChatGPT 憑據</string>
<string name="credential_hint_codex">推薦使用 OAuth 登入自動獲取憑據。也可手動貼上 auth.json 內容。</string> <string name="credential_hint_codex">推薦使用 OAuth 登入自動獲取憑據。也可手動貼上 auth.json 內容。</string>
<string name="action_oauth_login">🔐 OAuth 登入(推薦)</string> <string name="action_oauth_login">🔐 OAuth 登入(推薦)</string>
<string name="or_manual_import">── 或手動匯入 ──</string> <string name="or_manual_import">── 或手動匯入 ──</string>
@ -435,6 +435,7 @@
<string name="error_parse_models_empty">模型清單為空</string> <string name="error_parse_models_empty">模型清單為空</string>
<string name="error_parse_malformed">回應格式異常</string> <string name="error_parse_malformed">回應格式異常</string>
<string name="window_every_week">每週</string> <string name="window_every_week">每週</string>
<string name="window_spark">Spark</string>
<string name="msg_model_call_count">模型呼叫次數</string> <string name="msg_model_call_count">模型呼叫次數</string>
<string name="msg_stale_cache">快取數據 · %1$s</string> <string name="msg_stale_cache">快取數據 · %1$s</string>
<string name="msg_showing_last_balance">顯示的是上次成功獲取的餘額</string> <string name="msg_showing_last_balance">顯示的是上次成功獲取的餘額</string>

View File

@ -109,7 +109,7 @@
<string name="service_desc_deepseek">REST API · ¥</string> <string name="service_desc_deepseek">REST API · ¥</string>
<string name="service_desc_opencode_go">WebView fetch · 5h quota</string> <string name="service_desc_opencode_go">WebView fetch · 5h quota</string>
<string name="service_desc_commandcode_go">JSON API · $</string> <string name="service_desc_commandcode_go">JSON API · $</string>
<string name="service_desc_codex">ChatGPT Plus · Codex quota</string> <string name="service_desc_codex">ChatGPT · Codex quota</string>
<string name="service_desc_ollama">Cookie fetch · Cloud quota</string> <string name="service_desc_ollama">Cookie fetch · Cloud quota</string>
<!-- ══════════ 时长标签(DurationText 注入用) ══════════ --> <!-- ══════════ 时长标签(DurationText 注入用) ══════════ -->
@ -388,7 +388,7 @@
<string name="help_codex_12"> }}</string> <string name="help_codex_12"> }}</string>
<string name="help_codex_note">Tip: the app auto-refreshes the token after import,</string> <string name="help_codex_note">Tip: the app auto-refreshes the token after import,</string>
<string name="help_codex_note2">no manual updates needed.</string> <string name="help_codex_note2">no manual updates needed.</string>
<string name="credential_title_ollama">Ollama Pro credentials</string> <string name="credential_title_ollama">Ollama credentials</string>
<string name="credential_hint_ollama">Copy the full cookie from your browser. After logging into ollama.com/settings, find the Cookie header in DevTools → Network and copy it.</string> <string name="credential_hint_ollama">Copy the full cookie from your browser. After logging into ollama.com/settings, find the Cookie header in DevTools → Network and copy it.</string>
<string name="credential_update">Update credentials</string> <string name="credential_update">Update credentials</string>
<string name="credential_save">Save credentials</string> <string name="credential_save">Save credentials</string>
@ -417,7 +417,7 @@
<string name="api_key_form_title">Enter API Key</string> <string name="api_key_form_title">Enter API Key</string>
<string name="api_key_form_hint">Credentials are encrypted with Android Keystore (AES-256 GCM) and stored in local DataStore.</string> <string name="api_key_form_hint">Credentials are encrypted with Android Keystore (AES-256 GCM) and stored in local DataStore.</string>
<string name="action_update">Update</string> <string name="action_update">Update</string>
<string name="credential_title_codex">Codex / ChatGPT Plus credentials</string> <string name="credential_title_codex">Codex / ChatGPT credentials</string>
<string name="credential_hint_codex">OAuth login fetches credentials automatically. You can also paste auth.json manually.</string> <string name="credential_hint_codex">OAuth login fetches credentials automatically. You can also paste auth.json manually.</string>
<string name="action_oauth_login">🔐 OAuth login (recommended)</string> <string name="action_oauth_login">🔐 OAuth login (recommended)</string>
<string name="or_manual_import">── or import manually ──</string> <string name="or_manual_import">── or import manually ──</string>
@ -438,6 +438,7 @@
<string name="error_parse_models_empty">Model list is empty</string> <string name="error_parse_models_empty">Model list is empty</string>
<string name="error_parse_malformed">Response format is abnormal</string> <string name="error_parse_malformed">Response format is abnormal</string>
<string name="window_every_week">Weekly</string> <string name="window_every_week">Weekly</string>
<string name="window_spark">Spark</string>
<string name="msg_model_call_count">Model call count</string> <string name="msg_model_call_count">Model call count</string>
<string name="msg_stale_cache">Cached data · %1$s</string> <string name="msg_stale_cache">Cached data · %1$s</string>
<string name="msg_showing_last_balance">Showing the last successfully fetched balance</string> <string name="msg_showing_last_balance">Showing the last successfully fetched balance</string>