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