diff --git a/AGENTS.md b/AGENTS.md
index 0d033d6..a9deb84 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -92,6 +92,7 @@ Compact(手机):
↘ UsageData(原始数据)
→ CCGO: CCGO_USAGE_DETAIL(图表) → CCGO_USAGE_OVERVIEW(总统计)
↘ CCGO_USAGE_DATA(原始数据)
+ → Settings → Tips(使用小技巧)
返回用 guardedPop()(200ms 时间戳围栏,PopGuard 非 State 对象)+ Android predictive back。
Manifest 开启 `android:enableOnBackInvokedCallback="true"`;`navigation-compose` 保持 2.9.x 以上,使用后续 predictive back 修复。
@@ -103,7 +104,7 @@ PopGuard 额外检查 previousBackStackEntry != null,且 popBackStack() 返回
Expanded(平板,≥840dp):
┌─ 左侧 35%: Dashboard(固定) ─┐ ┌─ 右侧 65%: when(pane) 原子切换 ─────┐
│ │ │ ServiceDetail / OCGOUsage / CCGOUsage │
- │ │ │ Settings(内嵌 NavHost) │
+ │ │ │ Settings(内嵌 NavHost → Tips) │
└────────────────────────────────┘ └────────────────────────────────────────┘
右侧用量详情内部子路由:图表 → 总览 / 原始数据(OCGO/CCGO/Settings 各自用局部 NavHost)
面板切换用 when(pane) 分支(同一帧原子重组,零穿透),子路由由局部 NavHost 的 popBackStack() 内置防护。
@@ -118,9 +119,25 @@ Expanded(平板,≥840dp):
- 进度条颜色按百分比动态变化(<50% 草莓粉 / 50-80% 暖橙 / >80% 玫红)
- **MIUI Widget 适配**:`miuiWidget` 标识 → 可拖入负一屏;`miui.appwidget.action.APPWIDGET_UPDATE` 曝光刷新(划到即触发,20s 冷却);`@android:id/background` 根布局 ID(系统统一裁切圆角)
- **自动刷新**:`onUpdate()` 内缓存为空或超过 5 分钟冷却时自动发送 `WidgetRefreshReceiver` 广播
-- **一键添桌面**:Dashboard 顶部栏 + 按钮 → `requestPinAppWidget`(有 fallback 到 `ACTION_APPWIDGET_PICK`)
+- **一键添桌面**:Dashboard 顶部栏 + 按钮 → 二次确认弹窗 → 权限检测(Manifest `INSTALL_SHORTCUT` + MIUI AppOps `android:install_shortcut`)→ `requestPinAppWidget`(有 fallback 到 `ACTION_APPWIDGET_PICK`)
- 服务切换状态持久化到 SharedPreferences(`widget_auto_refresh` 中的 `display_service` key),切换后立即调用 `notifyDataChanged` 触发 `onUpdate()` 渲染
+**小组件点击绑定**:
+
+> `widget_wordmark` + `widget_open_hint`(`›` 箭头)→ 打开 APP;`widget_content` / `widget_switch` / `widget_service_title` → 切换服务广播 `ACTION_SWITCH_SERVICE`;`widget_refresh` → 刷新广播。
+> 切换 requestCode=2,刷新 requestCode=1。左上角 `›` 箭头是可点击进 APP 的视觉提示。
+
+**使用小技巧系统**:
+
+> - `AppTips`(`ui/components/AppTips.kt`)集中管理 13 条技巧,每条含 `title` / `hint`(一句话)/ `detail`(详细说明)
+> - Dashboard 卡片上方每次启动随机显示一条 `hint`(`remember { AppTips.randomHint() }`,不自动轮换)
+> - 设置页 `💡 使用小技巧` 卡片 → `TipsScreen` 独立页面(Route `tips`),LazyColumn 逐条展示
+> - 首次进入 Dashboard 显示一次性"长按卡片可拖拽排序"提示横幅,SharedPreferences `dashboard_ui_hints` 的 `drag_hint_shown` 标记
+
+**图表默认 tooltip**:
+
+> `StackedBarChart` / `LineChart` 的 `tooltipBucket` 初始值为 `buckets.lastOrNull()`,进入图表页即可看到最新时段数值详情,暗示图表可交互。
+
## RemoteViews 兼容性红线
以下元素在 Widget 布局中**不可用**,会导致「载入出现问题」:
diff --git a/README.md b/README.md
index 1c483e4..2455408 100644
--- a/README.md
+++ b/README.md
@@ -34,10 +34,11 @@ Android AI 余额与用量查询 APP —— 统一查看 DeepSeek、OpenCode Go
| 📋 **详细数据** | 原始记录分页浏览,支持时间 + 模型筛选,点击查看完整字段 |
| 🔍 **多粒度筛选** | 5小时 / 12小时(10分钟桶) / 24小时 / 今天 / 昨天 / 最近7天 / 当月 / 自定义日·月·范围 |
| 🏷️ **模型筛选** | 多选 / 单选 / 全选,动态图例自适应换行 |
-| 📱 **桌面小组件** | 不打开 APP 也能看用量;支持四服务切换(OCGO/CCGO/Codex/Ollama)+ DeepSeek 余额;可拖入负一屏;划到即自动刷新(MIUI 曝光刷新) |
+| 📱 **桌面小组件** | 不打开 APP 也能看用量;支持四服务切换(OCGO/CCGO/Codex/Ollama)+ DeepSeek 余额;左上角 › 进 APP、其他区域点切换、↻ 刷新;可拖入负一屏;划到即自动刷新(MIUI 曝光刷新) |
| 🔄 **自动同步** | 首页下拉自动同步用量;无缓存时启动自动全量同步;CCGO 详情页支持手动清除并重新同步 |
| 🌙 **深色模式** | 全局自适应 — App 内文字/图标/背景自动切换,小组件独立适配暗色布局 |
-| ➕ **一键添桌面** | APP 内点 + 直接添加小组件,不用去系统列表翻 |
+| ➕ **一键添桌面** | APP 内点 + 直接添加小组件,不用去系统列表翻;二次确认 + 权限检测 |
+| 💡 **使用小技巧** | 首页随机展示一条操作提示(每次启动刷新);设置页可查看全部 13 条隐藏操作技巧 |
| ⚡ **Room 数据库** | 用量记录存 Room(indexed on workspaceId+timeCreated),DAO 查询替代全量 JSON 序列化;首次启动自动从旧 DataStore 迁移 |
| 🎀 **雨晴粉主题** | Material Design 3 · 草莓粉 #FF85A2 · 樱粉 #FFD1DC |
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8531133..316fc4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
navController.navigate(Routes.credentialEdit(type)) }
+ onEditCredential = { type -> navController.navigate(Routes.credentialEdit(type)) },
+ onOpenTips = { navController.navigate(Routes.TIPS) }
)
}
+ composable(Routes.TIPS) {
+ TipsScreen(onBack = guardedPop)
+ }
composable(
route = Routes.CREDENTIAL_EDIT,
arguments = listOf(navArgument("type") { type = NavType.StringType })
@@ -415,9 +421,13 @@ private fun ExpandedDetailPane(
onBack = onClose,
onEditCredential = { type ->
settingsNavController.navigate(Routes.credentialEdit(type))
- }
+ },
+ onOpenTips = { settingsNavController.navigate("tips") }
)
}
+ composable("tips") {
+ TipsScreen(onBack = { settingsNavController.popBackStack() })
+ }
composable(
route = Routes.CREDENTIAL_EDIT,
arguments = listOf(navArgument("type") { type = NavType.StringType })
diff --git a/app/src/main/java/com/rainy/token/ui/components/AppTips.kt b/app/src/main/java/com/rainy/token/ui/components/AppTips.kt
new file mode 100644
index 0000000..a688fb8
--- /dev/null
+++ b/app/src/main/java/com/rainy/token/ui/components/AppTips.kt
@@ -0,0 +1,145 @@
+package com.rainy.token.ui.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.rainy.token.ui.theme.InkMuted
+import com.rainy.token.ui.theme.StrawberryPink
+
+/**
+ * APP 使用小技巧列表。
+ * Dashboard 卡片上方随机展示一条,设置页可查看全部。
+ */
+object AppTips {
+
+ data class Tip(
+ val title: String,
+ val hint: String,
+ val detail: String
+ )
+
+ val tips: List = listOf(
+ Tip("长按卡片拖拽排序", "长按首页卡片可以拖拽排序", "在首页长按任意服务卡片即可拖拽到想要的位置,排序会自动保存。"),
+ Tip("小组件点左上角进 APP", "小组件左上角标识可打开 APP", "桌面小组件左上角 RainyToken › 标识可以点击打开应用,其它区域点击切换服务。"),
+ Tip("图表点击查看详情", "点击图表柱子可查看时段详情", "在用量图表上点击或滑动手指,可查看对应时段的详细数值。"),
+ Tip("图表标题可点击", "点击图表标题弹出按模型明细", "图表卡片标题(如「消耗金额」)点击后会弹出按模型拆分的明细。"),
+ Tip("切换图表时区", "用量详情页可切换 UTC+0 / UTC+8", "用量详情页顶部可切换 UTC+0 / UTC+8,图表标签会跟随切换。"),
+ Tip("CCGO 清除并重新同步", "CCGO 详情页可清除缓存重新同步", "CCGO 用量详情页右上角「清除」可删除本地缓存并重新拉取全部数据。"),
+ Tip("查看用量详情", "服务卡片底部可进入用量图表", "有凭据的服务卡片底部会出现「查看用量详情」入口,点击进入图表统计。"),
+ Tip("原始数据浏览", "用量详情页右上角可查看原始记录", "用量详情页右上角「详细数据」可查看每条原始调用记录,支持分页跳转。"),
+ Tip("下拉刷新联动用量", "下拉刷新会同步余额和用量", "首页下拉刷新不仅刷新余额,还会自动同步 OCGO / CCGO 用量记录。"),
+ Tip("凭据获取教程", "凭据页底部有获取教程", "凭据编辑页底部的「如何获取」按钮有针对每个服务的详细操作步骤。"),
+ Tip("剪贴板自动导入", "Codex 支持从剪贴板一键导入", "Codex 凭据页支持「从剪贴板自动导入」,粘贴 auth.json 后一键填入。"),
+ Tip("详细数据页码跳转", "原始数据页可输入页码直跳", "原始数据页底部可输入页码直接跳转,数据量大时不必逐页翻。"),
+ Tip("小组件自动刷新", "划到小组件会自动刷新余额", "桌面小组件划到负一屏时会自动刷新余额,也可手动点 ↻ 按钮刷新。"),
+ )
+
+ /** 首页轮换用的一句话文案。 */
+ val shortHints: List = tips.map { it.hint }
+
+ /** 随机取一条短文案。 */
+ fun randomHint(): String = shortHints.random()
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TipsScreen(onBack: () -> Unit) {
+ Scaffold(
+ containerColor = Color.Transparent,
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ "使用小技巧",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ Icons.Filled.ArrowBack,
+ contentDescription = "返回",
+ tint = StrawberryPink
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent
+ )
+ )
+ }
+ ) { innerPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ itemsIndexed(AppTips.tips) { idx, tip ->
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = "${idx + 1}",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = StrawberryPink
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = tip.title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = tip.detail,
+ style = MaterialTheme.typography.bodyMedium,
+ color = InkMuted
+ )
+ }
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt
index af4fda0..8452771 100644
--- a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt
@@ -32,6 +32,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.TextButton
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -81,11 +82,16 @@ 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.components.AppTips
import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.StrawberryPink
+import android.app.AppOpsManager
import android.appwidget.AppWidgetManager
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Process
import android.widget.Toast
import com.rainy.token.ui.widget.OpenCodeGoWidgetProvider
import java.text.SimpleDateFormat
@@ -114,6 +120,15 @@ fun DashboardScreen(
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
+ var showAddWidgetConfirm by remember { mutableStateOf(false) }
+ // 一次性"长按拖拽排序"提示
+ var showDragHint by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ val prefs = context.getSharedPreferences("dashboard_ui_hints", android.content.Context.MODE_PRIVATE)
+ if (!prefs.getBoolean("drag_hint_shown", false)) {
+ showDragHint = true
+ }
+ }
val cardOrder = remember { mutableStateListOf() }
LaunchedEffect(Unit) {
val saved = context.getSharedPreferences(DASHBOARD_ORDER_PREFS, android.content.Context.MODE_PRIVATE)
@@ -136,6 +151,29 @@ fun DashboardScreen(
lastRefreshing = uiState.refreshing
}
+ if (showAddWidgetConfirm) {
+ AlertDialog(
+ onDismissRequest = { showAddWidgetConfirm = false },
+ title = { Text("添加桌面小组件?") },
+ text = { Text("确认后会请求桌面启动器添加雨晴Token小组件。部分系统仍会弹出系统确认窗口。") },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showAddWidgetConfirm = false
+ requestRainyTokenWidgetPin(context)
+ }
+ ) {
+ Text("确认添加", color = StrawberryPink)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showAddWidgetConfirm = false }) {
+ Text("取消")
+ }
+ }
+ )
+ }
+
Scaffold(
containerColor = Color.Transparent,
topBar = {
@@ -155,24 +193,7 @@ fun DashboardScreen(
}
},
actions = {
- // 添加小组件:优先 requestPinAppWidget,不支持则打开系统小组件选择器
- 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()
- }
- }
- }) {
+ IconButton(onClick = { showAddWidgetConfirm = true }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "添加小组件到桌面",
@@ -234,6 +255,14 @@ fun DashboardScreen(
.padding(contentPadding),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
+ // 随机小技巧提示(每次启动随机一条)
+ val tipText = remember { AppTips.randomHint() }
+ Text(
+ text = "💡 $tipText",
+ style = MaterialTheme.typography.bodySmall,
+ color = inkMuted(),
+ modifier = Modifier.padding(horizontal = 4.dp)
+ )
val items = rememberDashboardItems(
cards = uiState.cards,
order = cardOrder,
@@ -257,6 +286,41 @@ fun DashboardScreen(
.apply()
}
)
+ // 一次性"长按拖拽排序"提示横幅
+ if (showDragHint) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ showDragHint = false
+ context.getSharedPreferences("dashboard_ui_hints", android.content.Context.MODE_PRIVATE)
+ .edit().putBoolean("drag_hint_shown", true).apply()
+ },
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = StrawberryPink.copy(alpha = 0.12f)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "💡 长按卡片可拖拽排序",
+ style = MaterialTheme.typography.bodySmall,
+ color = StrawberryPink,
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = "知道了",
+ style = MaterialTheme.typography.bodySmall,
+ color = StrawberryPink,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+ }
// 底部 footer:填空白 + 提供版本号
DashboardFooter()
}
@@ -634,4 +698,46 @@ private fun DashboardFooter() {
color = inkMuted()
)
}
+}
+
+private fun canInstallLauncherShortcut(context: Context): Boolean {
+ val manifestPermissionGranted = context.packageManager.checkPermission(
+ "com.android.launcher.permission.INSTALL_SHORTCUT",
+ context.packageName
+ ) == PackageManager.PERMISSION_GRANTED
+ if (!manifestPermissionGranted) return false
+
+ val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
+ val mode = runCatching {
+ appOpsManager.checkOpNoThrow("android:install_shortcut", Process.myUid(), context.packageName)
+ }.getOrNull() ?: return true
+
+ return mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_DEFAULT
+}
+
+private fun requestRainyTokenWidgetPin(context: Context) {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val component = ComponentName(context, OpenCodeGoWidgetProvider::class.java)
+ if (!canInstallLauncherShortcut(context)) {
+ Toast.makeText(context, "桌面快捷方式权限已被拒绝,请在系统设置中允许后重试", Toast.LENGTH_LONG).show()
+ return
+ }
+
+ if (appWidgetManager.isRequestPinAppWidgetSupported) {
+ val requested = appWidgetManager.requestPinAppWidget(component, null, null)
+ if (!requested) {
+ Toast.makeText(context, "桌面未接受添加请求,请检查启动器权限", Toast.LENGTH_LONG).show()
+ }
+ return
+ }
+
+ val pickerIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
+ }
+ val canOpenPicker = pickerIntent.resolveActivity(context.packageManager) != null
+ if (canOpenPicker) {
+ context.startActivity(pickerIntent)
+ } else {
+ Toast.makeText(context, "当前桌面不支持应用内添加,请在桌面长按 → 小组件 → 查找雨晴Token", Toast.LENGTH_LONG).show()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/UsageChartScreen.kt b/app/src/main/java/com/rainy/token/ui/dashboard/UsageChartScreen.kt
index 60655db..9119ebb 100644
--- a/app/src/main/java/com/rainy/token/ui/dashboard/UsageChartScreen.kt
+++ b/app/src/main/java/com/rainy/token/ui/dashboard/UsageChartScreen.kt
@@ -547,7 +547,7 @@ internal fun StackedBarChart(
Text("暂无数据", color = InkMuted, style = MaterialTheme.typography.bodySmall)
return
}
- var tooltipBucket by remember { mutableStateOf(null) }
+ var tooltipBucket by remember { mutableStateOf(buckets.lastOrNull()) }
val density = LocalDensity.current
val d = density.density
val barCount = buckets.size
@@ -677,7 +677,7 @@ internal fun LineChart(
Text("暂无数据", color = InkMuted, style = MaterialTheme.typography.bodySmall)
return
}
- var tooltipBucket by remember { mutableStateOf(null) }
+ var tooltipBucket by remember { mutableStateOf(buckets.lastOrNull()) }
val density = LocalDensity.current
val d = density.density
val barCount = buckets.size
diff --git a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt
index c636639..77d16ea 100644
--- a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -42,6 +43,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.components.AppTips
import com.rainy.token.ui.theme.InkMuted
import com.rainy.token.ui.theme.StrawberryPink
@@ -58,6 +60,7 @@ import com.rainy.token.ui.theme.StrawberryPink
fun SettingsScreen(
onBack: () -> Unit,
onEditCredential: (ServiceType) -> Unit,
+ onOpenTips: () -> Unit = {},
viewModel: SettingsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -120,6 +123,9 @@ fun SettingsScreen(
onClick = { onEditCredential(status.service) }
)
}
+ item {
+ TipsCard(onClick = { onOpenTips() })
+ }
item {
Spacer(modifier = Modifier.padding(top = 8.dp))
AboutCard()
@@ -199,4 +205,41 @@ private fun stateToChip(state: CredentialStatus.State): StatusStyle = when (stat
CredentialStatus.State.OK -> StatusStyle("已配置", StatusLevel.OK)
CredentialStatus.State.EXPIRED -> StatusStyle("已过期", StatusLevel.ERROR)
CredentialStatus.State.WARNING -> StatusStyle("需重登", StatusLevel.WARNING)
+}
+
+@Composable
+private fun TipsCard(onClick: () -> Unit) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick() },
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = StrawberryPink.copy(alpha = 0.08f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "💡",
+ style = MaterialTheme.typography.titleLarge
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = "使用小技巧",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = StrawberryPink
+ )
+ Text(
+ text = "查看全部 ${AppTips.tips.size} 条隐藏操作",
+ style = MaterialTheme.typography.bodySmall,
+ color = InkMuted,
+ modifier = Modifier.padding(top = 2.dp)
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt b/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt
index 6790ab5..3b18184 100644
--- a/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt
+++ b/app/src/main/java/com/rainy/token/ui/widget/OpenCodeGoWidgetProvider.kt
@@ -60,15 +60,24 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
for (widgetId in appWidgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_opencode_go)
- // 点击 Widget → 打开 APP
- val intent = Intent(context, MainActivity::class.java).apply {
+ // 点击左上角品牌 → 打开 APP;点击其它内容区域 → 切换服务;刷新按钮单独刷新。
+ val openAppIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
- val pendingIntent = PendingIntent.getActivity(
- context, 0, intent,
+ val openAppPendingIntent = PendingIntent.getActivity(
+ context, 0, openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
- views.setOnClickPendingIntent(R.id.widget_content, pendingIntent)
+ views.setOnClickPendingIntent(R.id.widget_wordmark, openAppPendingIntent)
+ views.setOnClickPendingIntent(R.id.widget_open_hint, openAppPendingIntent)
+
+ val switchPendingIntent = PendingIntent.getBroadcast(
+ context, 2, Intent(context, OpenCodeGoWidgetProvider::class.java).apply { action = ACTION_SWITCH_SERVICE },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ views.setOnClickPendingIntent(R.id.widget_content, switchPendingIntent)
+ views.setOnClickPendingIntent(R.id.widget_switch, switchPendingIntent)
+ views.setOnClickPendingIntent(R.id.widget_service_title, switchPendingIntent)
// 刷新按钮 → 后台广播刷新
val refreshPendingIntent = PendingIntent.getBroadcast(
@@ -77,12 +86,6 @@ class OpenCodeGoWidgetProvider : AppWidgetProvider() {
)
views.setOnClickPendingIntent(R.id.widget_refresh, refreshPendingIntent)
- val switchPendingIntent = PendingIntent.getBroadcast(
- context, 2, Intent(context, OpenCodeGoWidgetProvider::class.java).apply { action = ACTION_SWITCH_SERVICE },
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- views.setOnClickPendingIntent(R.id.widget_switch, switchPendingIntent)
-
// 读缓存并填充数据
runBlocking {
try {
diff --git a/app/src/main/res/layout-night/widget_opencode_go.xml b/app/src/main/res/layout-night/widget_opencode_go.xml
index 7353014..3ee145b 100644
--- a/app/src/main/res/layout-night/widget_opencode_go.xml
+++ b/app/src/main/res/layout-night/widget_opencode_go.xml
@@ -30,6 +30,26 @@
android:contentDescription="RainyToken"
android:adjustViewBounds="true" />
+
+
+
+
+ android:layout_marginStart="6dp"
+ android:gravity="center" />
@@ -72,6 +93,7 @@
android:contentDescription="OpenCode Go"
android:layout_marginEnd="5dp" />
+
+
+ android:gravity="center" />
+ android:layout_marginStart="6dp"
+ android:gravity="center" />