feat: MIUI Widget 完整适配 + 深色模式全局修复

MIUI Widget 接入(负一屏 + 曝光刷新)
- AndroidManifest 新增 miuiWidget 全套标识
- intent-filter 新增 miui.appwidget.action.APPWIDGET_UPDATE
- 根布局改为 FrameLayout + @android:id/background
- minWidth 250→300dp(4x2 标准)
- onReceive() 处理 MIUI 曝光刷新
- 5min 冷却自动刷新

深色模式全面适配
- drawable-night/widget_bg.xml
- drawable-night/ic_opencode_go_logo.xml
- drawable-night/ic_deepseek_logo.png + wordmark
- layout-night/widget_opencode_go.xml
- DarkInkWarm/DarkInkMuted + inkWarm()/inkMuted()
- 全页面 51 处 InkMuted → inkMuted()
- ServiceIcon 白底 → 主题自适应

用户体验
- 顶部栏 + 按钮:一键添桌面小组件
- 负一屏底部遮挡修复(间距压缩 28dp)
This commit is contained in:
WaterRain 2026-06-19 07:38:02 +00:00
parent 05008ca20f
commit 5a18ffc02f
No known key found for this signature in database
20 changed files with 621 additions and 77 deletions

22
.commit_msg Normal file
View File

@ -0,0 +1,22 @@
feat: MIUI Widget 完整适配 + 深色模式全局修复
MIUI Widget 接入(负一屏 + 曝光刷新)
- AndroidManifest 新增 miuiWidget 全套标识
- intent-filter 新增 miui.appwidget.action.APPWIDGET_UPDATE
- 根布局改为 FrameLayout + @android:id/background
- minWidth 250→300dp(4x2 标准)
- onReceive() 处理 MIUI 曝光刷新
- 5min 冷却自动刷新
深色模式全面适配
- drawable-night/widget_bg.xml
- drawable-night/ic_opencode_go_logo.xml
- drawable-night/ic_deepseek_logo.png + wordmark
- layout-night/widget_opencode_go.xml
- DarkInkWarm/DarkInkMuted + inkWarm()/inkMuted()
- 全页面 51 处 InkMuted → inkMuted()
- ServiceIcon 白底 → 主题自适应
用户体验
- 顶部栏 + 按钮:一键添桌面小组件
- 负一屏底部遮挡修复(间距压缩 28dp)

View File

@ -28,22 +28,44 @@
</intent-filter>
</activity>
<!-- 桌面小组件 -->
<!-- 桌面小组件:后台刷新接收器(主进程,需要 Hilt DI) -->
<receiver
android:name=".ui.widget.WidgetRefreshReceiver"
android:exported="false" />
<!-- OpenCode Go 小组件(MIUI Widget) -->
<receiver
android:name=".ui.widget.OpenCodeGoWidgetProvider"
android:exported="true">
android:exported="true"
android:label="@string/widget_name">
<intent-filter>
<!-- 标准 Android 定时刷新 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!-- MIUI 曝光刷新(划到负一屏/桌面时触发) -->
<action android:name="miui.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_opencode_go_info" />
<!-- MIUI Widget 核心标识 -->
<meta-data
android:name="miuiWidget"
android:value="true" />
<!-- MIUI 曝光刷新:用户滑动到小组件所在页面时自动触发 -->
<meta-data
android:name="miuiWidgetRefresh"
android:value="exposure" />
<!-- 曝光刷新最小间隔 20 秒(官方最短为 10 秒) -->
<meta-data
android:name="miuiWidgetRefreshMinInterval"
android:value="20000" />
</receiver>
<!-- MIUI Widget 版本号(application 级,升级时递增) -->
<meta-data
android:name="miuiWidgetVersion"
android:value="1" />
</application>
</manifest>

View File

@ -2,6 +2,7 @@ package com.rainy.token.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
@ -40,7 +41,7 @@ fun ServiceIcon(
modifier = modifier
.size(size.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.White),
.background(if (isSystemInDarkTheme()) Color(0xFF352329) else Color.White),
contentAlignment = Alignment.Center
) {
Image(

View File

@ -22,6 +22,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Card
@ -59,8 +60,14 @@ import com.rainy.token.ui.components.ServiceIcon
import com.rainy.token.ui.components.StatusChip
import com.rainy.token.ui.components.StatusLevel
import com.rainy.token.ui.components.StatusStyle
import com.rainy.token.ui.theme.InkMuted
import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.StrawberryPink
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import com.rainy.token.ui.widget.OpenCodeGoWidgetProvider
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -110,11 +117,36 @@ fun DashboardScreen(
Text(
text = "AI 余额一览",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
},
actions = {
// 添加小组件:优先 requestPinAppWidget,不支持则打开系统小组件选择器
val context = LocalContext.current
IconButton(onClick = {
val appWidgetManager = AppWidgetManager.getInstance(context)
val component = ComponentName(context, OpenCodeGoWidgetProvider::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
appWidgetManager.requestPinAppWidget(component, null, null)
} else {
// MIUI 等桌面回退:直接打开小组件选择器
try {
val pickerIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
}
context.startActivity(pickerIntent)
} catch (e: Exception) {
Toast.makeText(context, "请在桌面长按 → 小组件 → 查找雨晴Token", Toast.LENGTH_LONG).show()
}
}
}) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "添加小组件到桌面",
tint = StrawberryPink
)
}
IconButton(
onClick = { viewModel.refresh() },
enabled = !uiState.refreshing
@ -165,7 +197,7 @@ fun DashboardScreen(
Text(
text = "服务余额",
style = MaterialTheme.typography.labelMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(top = 8.dp)
)
}
@ -204,7 +236,7 @@ private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit) {
Text(
text = secondaryLine(card),
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
StatusChip(style = card.statusBadgeStyle())
@ -223,7 +255,7 @@ private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit) {
color = if (card.lastFetchError != null)
MaterialTheme.colorScheme.error
else
InkMuted
inkMuted()
)
}
}
@ -237,24 +269,24 @@ private fun BalanceMainArea(card: DashboardCardUi) {
Text(
text = "—",
style = MaterialTheme.typography.displayMedium,
color = InkMuted
color = inkMuted()
)
Text(
text = "点击配置凭据",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
balance == null -> {
Text(
text = "—",
style = MaterialTheme.typography.displayMedium,
color = InkMuted
color = inkMuted()
)
Text(
text = "下拉刷新",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
card.service == ServiceType.OPENCODE_GO -> {
@ -278,7 +310,7 @@ private fun BalanceMainArea(card: DashboardCardUi) {
Text(
text = balance.unit,
style = MaterialTheme.typography.titleMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(bottom = 6.dp)
)
}
@ -293,7 +325,7 @@ private fun BalanceMainArea(card: DashboardCardUi) {
Text(
text = "本月已用 ${formatAmount(spent)}${balance.unit}",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
}
@ -313,7 +345,7 @@ private fun OpenCodeGoMainBalance(balance: com.rainy.token.domain.model.ServiceB
Text(
text = "%",
style = MaterialTheme.typography.titleLarge,
color = InkMuted,
color = inkMuted(),
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 6.dp, start = 2.dp)
)
@ -321,7 +353,7 @@ private fun OpenCodeGoMainBalance(balance: com.rainy.token.domain.model.ServiceB
Text(
text = "5h 用量",
style = MaterialTheme.typography.titleMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(bottom = 6.dp)
)
}
@ -356,13 +388,13 @@ private fun CompactUsageRowEmpty(label: String, resetInSec: Long?) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.weight(1f)
)
Text(
text = "—",
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
color = inkMuted()
)
}
Spacer(modifier = Modifier.height(4.dp))
@ -372,7 +404,7 @@ private fun CompactUsageRowEmpty(label: String, resetInSec: Long?) {
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp)),
color = InkMuted.copy(alpha = 0.3f),
color = inkMuted().copy(alpha = 0.3f),
trackColor = MaterialTheme.colorScheme.surfaceVariant,
strokeCap = androidx.compose.ui.graphics.StrokeCap.Butt
)
@ -381,7 +413,7 @@ private fun CompactUsageRowEmpty(label: String, resetInSec: Long?) {
Text(
text = "${formatResetInSec(resetInSec)}后重置",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
}
@ -398,7 +430,7 @@ private fun CompactUsageRow(label: String, pct: Int, resetInSec: Long?) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.weight(1f)
)
Text(
@ -433,7 +465,7 @@ private fun CompactUsageRow(label: String, pct: Int, resetInSec: Long?) {
Text(
text = "${formatResetInSec(resetInSec)}后重置",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
}
@ -511,13 +543,13 @@ private fun DashboardFooter() {
Text(
text = "RainyToken · v1.0",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "本地加密存储,不上传任何数据",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
}

View File

@ -52,7 +52,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.rainy.token.data.local.UsageRecord
import com.rainy.token.ui.theme.InkMuted
import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.StrawberryPink
import java.text.SimpleDateFormat
import java.time.Instant
@ -121,7 +121,7 @@ fun UsageDataScreen(
})
}
}
Text("UTC+0", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("UTC+0", style = MaterialTheme.typography.bodySmall, color = inkMuted())
Spacer(Modifier.weight(1f))
// 模型筛选
Box {
@ -150,17 +150,17 @@ fun UsageDataScreen(
CustomTimeRangeRow(customStartMs, customEndMs,
{ showStartPicker = true }, { showEndPicker = true },
{ viewModel.setTimeFilter(TimeFilter.Custom(customStartMs, customEndMs)) })
Text("所有时间按 UTC+0 计算", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("所有时间按 UTC+0 计算", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
// ─── 记录数 + 分页 ───
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
Text("${state.totalRecords} 条记录", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("${state.totalRecords} 条记录", style = MaterialTheme.typography.bodySmall, color = inkMuted())
if (state.totalPages > 1) {
Row(verticalAlignment = Alignment.CenterVertically) {
TextButton(onClick = { viewModel.prevPage() }, enabled = state.currentPage > 1) {
Text("◀", color = StrawberryPink, fontSize = 13.sp) }
Text("${state.currentPage}/${state.totalPages}", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("${state.currentPage}/${state.totalPages}", style = MaterialTheme.typography.bodySmall, color = inkMuted())
TextButton(onClick = { viewModel.nextPage() }, enabled = state.currentPage < state.totalPages) {
Text("▶", color = StrawberryPink, fontSize = 13.sp) }
Spacer(Modifier.width(6.dp))
@ -173,7 +173,7 @@ fun UsageDataScreen(
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = androidx.compose.ui.text.input.KeyboardType.Number),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = StrawberryPink,
unfocusedBorderColor = InkMuted.copy(alpha = 0.4f)
unfocusedBorderColor = inkMuted().copy(alpha = 0.4f)
)
)
TextButton(onClick = {
@ -201,11 +201,11 @@ fun UsageDataScreen(
// ─── 记录列表 ───
if (state.loading) {
Box(Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
Text("加载中…", color = InkMuted)
Text("加载中…", color = inkMuted())
}
} else if (state.records.isEmpty()) {
Box(Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
Text("暂无数据", color = InkMuted)
Text("暂无数据", color = inkMuted())
}
} else {
LazyColumn(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(1.dp)) {
@ -265,7 +265,7 @@ fun UsageDataScreen(
@Composable
private fun HeaderCell(text: String, modifier: Modifier) {
Text(text, modifier = modifier, fontSize = 12.sp, fontWeight = FontWeight.Bold,
color = InkMuted, textAlign = TextAlign.Center)
color = inkMuted(), textAlign = TextAlign.Center)
}
@Composable
@ -293,7 +293,7 @@ private fun DataCell(text: String, modifier: Modifier) {
@Composable
private fun RawField(label: String, value: String) {
Row(Modifier.fillMaxWidth().padding(vertical = 1.dp)) {
Text("$label: ", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = InkMuted)
Text("$label: ", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = inkMuted())
Text(value, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface)
}
}

View File

@ -43,7 +43,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.rainy.token.data.local.ModelStats
import com.rainy.token.data.local.OverviewStats
import com.rainy.token.ui.theme.InkMuted
import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.StrawberryPink
import java.text.SimpleDateFormat
import java.util.Date
@ -89,7 +89,7 @@ fun UsageOverviewScreen(
Text(uiState.timeFilter.label, color = StrawberryPink, fontWeight = FontWeight.SemiBold)
Icon(Icons.Filled.ArrowDropDown, null, tint = StrawberryPink)
}
Text("UTC+0", style = MaterialTheme.typography.bodySmall, color = InkMuted, modifier = Modifier.padding(start = 4.dp))
Text("UTC+0", style = MaterialTheme.typography.bodySmall, color = inkMuted(), modifier = Modifier.padding(start = 4.dp))
DropdownMenu(menuExpanded, { menuExpanded = false }) {
listOf(TimeFilter.All, TimeFilter.Last5h, TimeFilter.Last24h,
TimeFilter.Today, TimeFilter.Yesterday,
@ -112,7 +112,7 @@ fun UsageOverviewScreen(
CustomTimeRangeRow(customStartMs, customEndMs,
{ showStartPicker = true }, { showEndPicker = true },
{ viewModel.setTimeFilter(TimeFilter.Custom(customStartMs, customEndMs)) })
Text("所有时间按 UTC+0 计算", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("所有时间按 UTC+0 计算", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
}
// 总览
@ -157,7 +157,7 @@ fun UsageOverviewScreen(
Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) {
TextButton(onClick = { viewModel.prevDailyPage() }, enabled = uiState.dailyPage > 1) {
Text("◀ 上一页", color = StrawberryPink, style = MaterialTheme.typography.bodySmall) }
Text("${uiState.dailyPage} / $totalPages", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("${uiState.dailyPage} / $totalPages", style = MaterialTheme.typography.bodySmall, color = inkMuted())
TextButton(onClick = { viewModel.nextDailyPage() }, enabled = uiState.dailyPage < totalPages) {
Text("下一页 ▶", color = StrawberryPink, style = MaterialTheme.typography.bodySmall) }
}
@ -193,7 +193,7 @@ fun UsageOverviewScreen(
@Composable private fun CacheBreakdownCard(overview: OverviewStats) {
Card(Modifier.fillMaxWidth(), RoundedCornerShape(20.dp), CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) {
Column(Modifier.padding(16.dp)) {
Text("缓存 Token 细分", style = MaterialTheme.typography.labelMedium, color = InkMuted)
Text("缓存 Token 细分", style = MaterialTheme.typography.labelMedium, color = inkMuted())
Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), Arrangement.SpaceEvenly) {
StatItem("缓存读取", formatTokenCount(overview.cacheReadTokens))
@ -201,7 +201,7 @@ fun UsageOverviewScreen(
}
if (overview.cacheReadTokens > 0) {
Spacer(Modifier.height(4.dp))
Text("※ 缓存读取已计入「输入 Token」", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("※ 缓存读取已计入「输入 Token」", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
}
}
@ -216,11 +216,11 @@ fun UsageOverviewScreen(
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text(stat.model, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
Text("${stat.count} 次调用", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("${stat.count} 次调用", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
Column(horizontalAlignment = Alignment.End) {
Text(formatTokenCount(stat.totalTokens), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, color = StrawberryPink)
Text("$${String.format(Locale.US, "%.4f", stat.totalCost / 100_000_000.0)}", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("$${String.format(Locale.US, "%.4f", stat.totalCost / 100_000_000.0)}", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
}
}
@ -233,7 +233,7 @@ fun UsageOverviewScreen(
Text(utcFmt.format(Date(day.dayTs)), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f))
Column(horizontalAlignment = Alignment.End) {
Text(formatTokenCount(day.totalTokens), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, color = StrawberryPink)
Text("${day.count} 次 · $${String.format(Locale.US, "%.4f", day.totalCost / 100_000_000.0)}", style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text("${day.count} 次 · $${String.format(Locale.US, "%.4f", day.totalCost / 100_000_000.0)}", style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
}
}
@ -242,7 +242,7 @@ fun UsageOverviewScreen(
@Composable private fun StatItem(label: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(value, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = StrawberryPink)
Text(label, style = MaterialTheme.typography.bodySmall, color = InkMuted)
Text(label, style = MaterialTheme.typography.bodySmall, color = inkMuted())
}
}

View File

@ -51,7 +51,7 @@ import com.rainy.token.ui.components.ServiceIcon
import com.rainy.token.ui.components.StatusChip
import com.rainy.token.ui.components.StatusLevel
import com.rainy.token.ui.components.StatusStyle
import com.rainy.token.ui.theme.InkMuted
import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.StrawberryPink
import java.text.SimpleDateFormat
import java.util.Date
@ -187,7 +187,7 @@ private fun OpenCodeGoWindowsCard(state: State) {
Text(
text = "用量窗口",
style = MaterialTheme.typography.labelLarge,
color = InkMuted
color = inkMuted()
)
Spacer(modifier = Modifier.height(12.dp))
UsageWindowRow(
@ -226,7 +226,7 @@ private fun UsageWindowRow(label: String, pct: Int?, resetInSec: Long?) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.weight(1f)
)
Row(verticalAlignment = Alignment.Bottom) {
@ -238,7 +238,7 @@ private fun UsageWindowRow(label: String, pct: Int?, resetInSec: Long?) {
Text(
text = "%",
style = MaterialTheme.typography.bodyMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(bottom = 2.dp, start = 2.dp)
)
}
@ -262,7 +262,7 @@ private fun UsageWindowRow(label: String, pct: Int?, resetInSec: Long?) {
Text(
text = "${formatResetInSec(resetInSec)}后重置",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
}
@ -293,7 +293,7 @@ private fun MainBalanceCard(state: State, service: ServiceType) {
Text(
text = mainCardLabel(service),
style = MaterialTheme.typography.labelLarge,
color = InkMuted
color = inkMuted()
)
Spacer(modifier = Modifier.weight(1f))
StatusChip(style = stateToChip(state))
@ -322,14 +322,14 @@ private fun MainBalanceCard(state: State, service: ServiceType) {
Text(
text = "显示的是上次成功获取的余额",
style = MaterialTheme.typography.bodySmall,
color = InkMuted
color = inkMuted()
)
}
is State.ManualModeHint -> {
Text(
text = "—",
style = MaterialTheme.typography.displayMedium,
color = InkMuted
color = inkMuted()
)
}
}
@ -343,7 +343,7 @@ private fun BalanceBigNumber(balance: ServiceBalance?) {
Text(
text = "—",
style = MaterialTheme.typography.displayLarge,
color = InkMuted
color = inkMuted()
)
return
}
@ -358,7 +358,7 @@ private fun BalanceBigNumber(balance: ServiceBalance?) {
Text(
text = balance.unit,
style = MaterialTheme.typography.titleLarge,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(bottom = 8.dp)
)
}
@ -375,7 +375,7 @@ private fun BalanceBigNumber(balance: ServiceBalance?) {
Text(
text = "本月消费 ${formatAmount(spent)}${balance.unit}",
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
color = inkMuted()
)
}
}
@ -406,7 +406,7 @@ private fun DeepSeekBreakdownCard(state: State) {
Text(
text = "余额构成",
style = MaterialTheme.typography.labelLarge,
color = InkMuted
color = inkMuted()
)
Spacer(modifier = Modifier.height(12.dp))
BreakdownRow("赠送余额", granted, balance!!.unit)
@ -427,7 +427,7 @@ private fun BreakdownRow(label: String, value: Double, unit: String) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.weight(1f)
)
Row(verticalAlignment = Alignment.Bottom) {
@ -440,7 +440,7 @@ private fun BreakdownRow(label: String, value: Double, unit: String) {
Text(
text = unit,
style = MaterialTheme.typography.bodyMedium,
color = InkMuted,
color = inkMuted(),
modifier = Modifier.padding(bottom = 2.dp)
)
}

View File

@ -37,3 +37,6 @@ val DarkBackground = Color(0xFF1F1419) // 暖深背景
val DarkSurface = Color(0xFF2A1F25) // 卡片
val DarkOnSurface = Color(0xFFEFE0E5) // 卡片文本
val DarkPrimary = Color(0xFFFFB3C6) // 暗色下主色更亮
val DarkInkWarm = Color(0xFFEFE0E5) // 暗色下暖黑→浅粉白
val DarkInkMuted = Color(0xFFC9B8BE) // 暗色下暖灰→浅灰粉
val DarkInkOutline = Color(0xFF4D3A42) // 暗色下描边

View File

@ -88,4 +88,14 @@ fun RainyTokenTheme(
typography = RainyTypography,
content = content
)
}
}
/**
* 暗色模式自适应的文本颜色。
* 浅色模式下返回暖黑/暖灰,深色模式下自动切换到浅色。
*/
@Composable
fun inkWarm(): Color = if (isSystemInDarkTheme()) DarkInkWarm else InkWarm
@Composable
fun inkMuted(): Color = if (isSystemInDarkTheme()) DarkInkMuted else InkMuted

View File

@ -19,13 +19,30 @@ import java.util.Date
import java.util.Locale
/**
* OpenCode Go 桌面小组件。
* OpenCode Go 桌面小组件(MIUI Widget)。
*
* 显示 3 个用量窗口(5h / 本周 / 本月)的百分比 + 进度条 + 重置倒计时。
* 优先展示缓存数据;缓存为空或超过 5 分钟冷却时间时,自动触发后台网络刷新。
* 显示 3 个用量窗口(5h / 本周 / 本月)的百分比 + 进度条 + 重置倒计时 + DeepSeek 余额。
*
* 刷新路径:
* - MIUI 曝光刷新:用户划到负一屏 → miui.appwidget.action.APPWIDGET_UPDATE → onReceive → onUpdate
* - 标准定时刷新:系统 30min 定时 → android.appwidget.action.APPWIDGET_UPDATE → onUpdate
* - 手动 ↻ 按钮:PendingIntent 直通 WidgetRefreshReceiver → 网络请求 → notifyDataChanged
* - APP 内刷新:notifyDataChanged() → 广播 ACTION_APPWIDGET_UPDATE → onUpdate
* - 缓存为空/过期时 onUpdate 自动触发后台刷新
*/
class OpenCodeGoWidgetProvider : AppWidgetProvider() {
override fun onReceive(context: Context, intent: Intent) {
if ("miui.appwidget.action.APPWIDGET_UPDATE" == intent.action) {
val appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
if (appWidgetIds != null) {
onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds)
}
} else {
super.onReceive(context, intent)
}
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
@ -44,7 +61,7 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
views.setOnClickPendingIntent(R.id.widget_content, pendingIntent)
// 刷新按钮 → 后台广播刷新
val refreshPendingIntent = PendingIntent.getBroadcast(

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- OpenCode Go logo 深色模式版:深色填充 → 浅色,保证在 #2D1825 背景上可见 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="54dp"
android:height="30dp"
android:viewportWidth="54"
android:viewportHeight="30">
<path
android:fillColor="#E8D8D5"
android:pathData="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z"/>
<path
android:fillColor="#A09098"
android:pathData="M12 18H18V24H6V12H12V18Z"/>
<path
android:fillColor="#A09098"
android:pathData="M48 12V24H36V12H48Z"/>
<path
android:fillColor="#E8D8D5"
android:pathData="M54 30H30V0H54V30ZM36 24H48V6H36V24Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- MIUI Widget 深色模式背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="#2D1825" />
<stroke
android:width="1dp"
android:color="#4D2D3D" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- MIUI Widget 深色模式布局 — 文字颜色适配暗色背景 #2D1825 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_bg">
<LinearLayout
android:id="@+id/widget_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:gravity="center_vertical">
<!-- 第一行:wordmark + 更新时间 + 刷新按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="2dp">
<ImageView
android:id="@+id/widget_wordmark"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:src="@drawable/rainytoken_wordmark"
android:contentDescription="RainyToken"
android:adjustViewBounds="true" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:max="100" android:progress="0"
android:progressTint="#00000000"
android:progressBackgroundTint="#00000000"
android:indeterminate="false" />
<TextView
android:id="@+id/widget_updated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textColor="#D4C4CC"
android:textSize="10sp" />
<TextView
android:id="@+id/widget_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="↻"
android:textColor="#FF85A2"
android:textSize="20sp"
android:layout_marginStart="6dp" />
</LinearLayout>
<!-- OpenCode Go 标识 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="6dp">
<ImageView
android:id="@+id/widget_logo"
android:layout_width="22dp" android:layout_height="12dp"
android:src="@drawable/ic_opencode_go_logo"
android:contentDescription="OpenCode Go"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OpenCode Go · 用量"
android:textColor="#C4A0AB"
android:textSize="11sp" />
</LinearLayout>
<!-- 5h -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row1_label"
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="5h" android:textColor="#C4A0AB" android:textSize="11sp" />
<TextView android:id="@+id/row1_pct"
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#FFD1DC" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar android:id="@+id/row1_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="0"
android:progressTint="@color/widget_bar_5h" />
<TextView android:id="@+id/row1_reset"
android:layout_width="48dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#8A7A82" android:textSize="10sp" />
</LinearLayout>
<!-- 本周 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row2_label"
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="本周" android:textColor="#C4A0AB" android:textSize="11sp" />
<TextView android:id="@+id/row2_pct"
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#FFD1DC" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar android:id="@+id/row2_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="0"
android:progressTint="@color/widget_bar_weekly" />
<TextView android:id="@+id/row2_reset"
android:layout_width="48dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#8A7A82" android:textSize="10sp" />
</LinearLayout>
<!-- 本月 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row3_label"
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="本月" android:textColor="#C4A0AB" android:textSize="11sp" />
<TextView android:id="@+id/row3_pct"
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#FFD1DC" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar android:id="@+id/row3_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="0"
android:progressTint="@color/widget_bar_monthly" />
<TextView android:id="@+id/row3_reset"
android:layout_width="48dp" android:layout_height="wrap_content"
android:text="--" android:textColor="#8A7A82" android:textSize="10sp" />
</LinearLayout>
<!-- DeepSeek 余额 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="2dp">
<ImageView
android:layout_width="wrap_content" android:layout_height="12dp"
android:src="@drawable/ic_deepseek_logo"
android:contentDescription="DeepSeek"
android:adjustViewBounds="true"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DeepSeek 余额"
android:textColor="#C4A0AB"
android:textSize="11sp" />
<TextView
android:id="@+id/widget_ds_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textColor="#FFD1DC"
android:textSize="11sp"
android:textStyle="bold"
android:layout_marginStart="4dp" />
</LinearLayout>
<!-- 署名 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:max="100" android:progress="0"
android:progressTint="#00000000"
android:progressBackgroundTint="#00000000"
android:indeterminate="false" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="雨晴喵"
android:textColor="#8A7A82"
android:textSize="9sp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -1,11 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_root"
<!-- MIUI Widget 布局规范:根布局必须为 FrameLayout + @android:id/background -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_bg"
android:orientation="vertical"
android:padding="12dp">
android:background="@drawable/widget_bg">
<LinearLayout
android:id="@+id/widget_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:gravity="center_vertical">
<!-- 注意:保持和原来一样的内部结构,只是套了一层 FrameLayout -->
<!-- 第一行 -->
<LinearLayout
@ -59,7 +67,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
android:layout_marginBottom="6dp">
<ImageView
android:id="@+id/widget_logo"
@ -83,7 +91,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="6dp">
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row1_label"
android:layout_width="36dp" android:layout_height="wrap_content"
@ -112,7 +120,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="6dp">
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row2_label"
android:layout_width="36dp" android:layout_height="wrap_content"
@ -141,7 +149,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
android:layout_marginBottom="3dp">
<TextView android:id="@+id/row3_label"
android:layout_width="36dp" android:layout_height="wrap_content"
@ -170,7 +178,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
android:layout_marginBottom="2dp">
<ImageView
android:layout_width="wrap_content"
@ -224,4 +232,6 @@
android:textSize="9sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 小组件预览布局 — 小米小部件中心选择器展示用 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_bg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:gravity="center_vertical">
<!-- 标题行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="2dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="24dp"
android:src="@drawable/rainytoken_wordmark"
android:adjustViewBounds="true" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:max="100"
android:progress="0"
android:progressTint="#00000000"
android:progressBackgroundTint="#00000000"
android:indeterminate="false" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动刷新"
android:textColor="#C4A0AB"
android:textSize="10sp" />
</LinearLayout>
<!-- OpenCode Go 标识 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
<ImageView
android:layout_width="22dp"
android:layout_height="12dp"
android:src="@drawable/ic_opencode_go_logo"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OpenCode Go · 用量"
android:textColor="#8A7A82"
android:textSize="11sp" />
</LinearLayout>
<!-- 5h -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="6dp">
<TextView
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="5h" android:textColor="#8A7A82" android:textSize="11sp" />
<TextView
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="42%" android:textColor="#3D2C35" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="42"
android:progressTint="#FF85A2" />
</LinearLayout>
<!-- 本周 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="6dp">
<TextView
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="本周" android:textColor="#8A7A82" android:textSize="11sp" />
<TextView
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="68%" android:textColor="#3D2C35" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="68"
android:progressTint="#FFA726" />
</LinearLayout>
<!-- 本月 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="36dp" android:layout_height="wrap_content"
android:text="本月" android:textColor="#8A7A82" android:textSize="11sp" />
<TextView
android:layout_width="38dp" android:layout_height="wrap_content"
android:text="35%" android:textColor="#3D2C35" android:textSize="12sp"
android:textStyle="bold" android:gravity="end" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp" android:layout_height="6dp"
android:layout_weight="1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:max="100" android:progress="35"
android:progressTint="#FF85A2" />
</LinearLayout>
<!-- DeepSeek 余额 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="12dp"
android:src="@drawable/ic_deepseek_logo"
android:adjustViewBounds="true"
android:layout_marginEnd="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DeepSeek 余额"
android:textColor="#8A7A82"
android:textSize="11sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="¥18.00"
android:textColor="#3D2C35"
android:textSize="11sp"
android:textStyle="bold"
android:layout_marginStart="4dp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -1,3 +1,5 @@
<resources>
<string name="app_name">雨晴Token</string>
<string name="widget_name">AI用量面板</string>
<string name="widget_description">OpenCode Go 用量 + DeepSeek 余额,自动刷新</string>
</resources>

View File

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minWidth="300dp"
android:minHeight="110dp"
android:initialLayout="@layout/widget_opencode_go"
android:previewImage="@mipmap/ic_launcher"
android:previewImage="@drawable/widget_preview"
android:previewLayout="@layout/widget_opencode_go_preview"
android:description="@string/widget_description"
android:resizeMode="vertical|horizontal"
android:widgetCategory="home_screen"
android:widgetCategory="home_screen|keyguard"
android:widgetFeatures="reconfigurable"
android:updatePeriodMillis="1800000" />