Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
34 KiB
JSON

{"repo": "657kbps/quota-widget", "files": {"shared/src/androidMain/kotlin/com/kuyermqi/quotawidget/settings/AndroidPlatformSettingsRepository.kt": {"2e2f0c63babec862fe1d8b63edd172ae8fbf4238": {"t": 1786068310.2932906, "n": 34099, "h": "9a45900312ffd828", "c": "package com.kuyermqi.quotawidget.settings\n\nimport android.content.Context\nimport android.util.Base64\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.MutablePreferences\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.booleanPreferencesKey\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.core.intPreferencesKey\nimport androidx.datastore.preferences.core.longPreferencesKey\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport androidx.datastore.preferences.preferencesDataStore\nimport com.google.crypto.tink.Aead\nimport com.google.crypto.tink.KeyTemplates\nimport com.google.crypto.tink.aead.AeadConfig\nimport com.google.crypto.tink.integration.android.AndroidKeysetManager\nimport com.kuyermqi.quotawidget.domain.ALLOWED_REFRESH_INTERVAL_MINUTES\nimport com.kuyermqi.quotawidget.domain.AppSettings\nimport com.kuyermqi.quotawidget.domain.CurrencyPreference\nimport com.kuyermqi.quotawidget.domain.DarkThemeMode\nimport com.kuyermqi.quotawidget.domain.DEFAULT_CUSTOM_SEED_COLOR_ARGB\nimport com.kuyermqi.quotawidget.domain.DEFAULT_REFRESH_INTERVAL_MINUTES\nimport com.kuyermqi.quotawidget.domain.UsageDisplayMode\nimport com.kuyermqi.quotawidget.domain.UsageProgressStyle\nimport com.kuyermqi.quotawidget.domain.UsageWindowKind\nimport com.kuyermqi.quotawidget.domain.QuotaSnapshot\nimport com.kuyermqi.quotawidget.domain.QuotaWindow\nimport com.kuyermqi.quotawidget.domain.QuotaWindowKind\nimport com.kuyermqi.quotawidget.domain.RefreshIconPhase\nimport com.kuyermqi.quotawidget.domain.ThemeColorMode\nimport com.kuyermqi.quotawidget.domain.WidgetDisplayState\nimport com.kuyermqi.quotawidget.domain.decodeQuotaWindows\nimport com.kuyermqi.quotawidget.domain.encodeQuotaWindows\nimport com.kuyermqi.quotawidget.domain.formatBalance\nimport com.kuyermqi.quotawidget.platform.PlatformIds\nimport com.kuyermqi.quotawidget.platform.PlatformRegistry\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\n\nprivate val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = \"quota_widget_settings\")\n\nclass AndroidPlatformSettingsRepository(\n context: Context,\n) : PlatformSettingsRepository {\n private val appContext = context.applicationContext\n private val dataStore = appContext.dataStore\n private val aead: Aead by lazy { createAead(appContext) }\n\n override fun observeDeepSeekSettings(): Flow<DeepSeekSettings> =\n dataStore.data.map { prefs -> prefs.toDeepSeekSettings() }\n\n override suspend fun getDeepSeekSettings(): DeepSeekSettings =\n dataStore.data.first().toDeepSeekSettings()\n\n override suspend fun saveDeepSeekSettings(settings: DeepSeekSettings) {\n dataStore.edit { prefs ->\n if (settings.apiKey.isBlank()) {\n prefs.remove(Keys.API_KEY_ENCRYPTED)\n prefs.clearWidgetPayload(PlatformIds.DEEPSEEK)\n prefs[widgetStatusKey(PlatformIds.DEEPSEEK)] = Status.NOT_CONFIGURED\n } else {\n prefs[Keys.API_KEY_ENCRYPTED] = encrypt(settings.apiKey, API_KEY_AD)\n val status = prefs[widgetStatusKey(PlatformIds.DEEPSEEK)]\n if (status == null || status == Status.NOT_CONFIGURED) {\n prefs[widgetStatusKey(PlatformIds.DEEPSEEK)] = Status.LOADING\n }\n }\n prefs[Keys.CURRENCY] = settings.currency.name\n }\n }\n\n override fun observeOpenCodeGoSettings(): Flow<OpenCodeGoSettings> =\n dataStore.data.map { prefs -> prefs.toOpenCodeGoSettings() }\n\n override suspend fun getOpenCodeGoSettings(): OpenCodeGoSettings =\n dataStore.data.first().toOpenCodeGoSettings()\n\n override suspend fun saveOpenCodeGoSettings(settings: OpenCodeGoSettings) {\n dataStore.edit { prefs ->\n if (settings.workspaceId.isBlank() || settings.authCookie.isBlank()) {\n prefs.remove(Keys.OPENCODE_WORKSPACE_ID)\n prefs.remove(Keys.OPENCODE_WORKSPACE_NAME)\n prefs.remove(Keys.OPENCODE_AUTH_COOKIE_ENC)\n prefs.clearWidgetPayload(PlatformIds.OPENCODE_GO)\n prefs[widgetStatusKey(PlatformIds.OPENCODE_GO)] = Status.NOT_CONFIGURED\n } else {\n prefs[Keys.OPENCODE_WORKSPACE_ID] = settings.workspaceId\n if (settings.workspaceName.isBlank()) {\n prefs.remove(Keys.OPENCODE_WORKSPACE_NAME)\n } else {\n prefs[Keys.OPENCODE_WORKSPACE_NAME] = settings.workspaceName\n }\n prefs[Keys.OPENCODE_AUTH_COOKIE_ENC] = encrypt(settings.authCookie, OPENCODE_AUTH_AD)\n val status = prefs[widgetStatusKey(PlatformIds.OPENCODE_GO)]\n if (status == null ||\n status == Status.NOT_CONFIGURED ||\n status == Status.NEEDS_REAUTH\n ) {\n prefs[widgetStatusKey(PlatformIds.OPENCODE_GO)] = Status.LOADING\n }\n }\n prefs[Keys.OPENCODE_WIDGET_WINDOW] = settings.widgetWindowKind.name\n prefs[Keys.OPENCODE_USAGE_DISPLAY] = settings.usageDisplayMode.name\n prefs[Keys.OPENCODE_USAGE_PROGRESS_STYLE] = settings.usageProgressStyle.name\n }\n }\n\n override suspend fun clearOpenCodeGoSettings() {\n val current = getOpenCodeGoSettings()\n saveOpenCodeGoSettings(\n OpenCodeGoSettings(\n widgetWindowKind = current.widgetWindowKind,\n usageDisplayMode = current.usageDisplayMode,\n usageProgressStyle = current.usageProgressStyle,\n ),\n )\n }\n\n override fun observeCodexSettings(): Flow<CodexSettings> =\n dataStore.data.map { prefs -> prefs.toCodexSettings() }\n\n override suspend fun getCodexSettings(): CodexSettings =\n dataStore.data.first().toCodexSettings()\n\n override suspend fun saveCodexSettings(settings: CodexSettings) {\n dataStore.edit { prefs ->\n if (!settings.isConfigured) {\n prefs.remove(Keys.CODEX_ACCESS_ENC)\n prefs.remove(Keys.CODEX_REFRESH_ENC)\n prefs.remove(Keys.CODEX_ID_ENC)\n prefs.remove(Keys.CODEX_ACCOUNT_ID)\n prefs.remove(Keys.CODEX_EXPIRES_AT)\n prefs.remove(Keys.CODEX_EMAIL)\n prefs.remove(Keys.CODEX_PLAN_TYPE)\n prefs.clearWidgetPayload(PlatformIds.CODEX)\n prefs[widgetStatusKey(PlatformIds.CODEX)] = Status.NOT_CONFIGURED\n } else {\n prefs[Keys.CODEX_ACCESS_ENC] = encrypt(settings.accessToken, CODEX_ACCESS_AD)\n prefs[Keys.CODEX_REFRESH_ENC] = encrypt(settings.refreshToken, CODEX_REFRESH_AD)\n if (settings.idToken.isBlank()) {\n prefs.remove(Keys.CODEX_ID_ENC)\n } else {\n prefs[Keys.CODEX_ID_ENC] = encrypt(settings.idToken, CODEX_ID_AD)\n }\n prefs[Keys.CODEX_ACCOUNT_ID] = settings.accountId\n prefs[Keys.CODEX_EXPIRES_AT] = settings.expiresAtEpochMs\n if (settings.email.isBlank()) {\n prefs.remove(Keys.CODEX_EMAIL)\n } else {\n prefs[Keys.CODEX_EMAIL] = settings.email\n }\n if (settings.planType.isBlank()) {\n prefs.remove(Keys.CODEX_PLAN_TYPE)\n } else {\n prefs[Keys.CODEX_PLAN_TYPE] = settings.planType\n }\n val status = prefs[widgetStatusKey(PlatformIds.CODEX)]\n if (status == null ||\n status == Status.NOT_CONFIGURED ||\n status == Status.NEEDS_REAUTH\n ) {\n prefs[widgetStatusKey(PlatformIds.CODEX)] = Status.LOADING\n }\n }\n prefs[Keys.CODEX_WIDGET_WINDOW] = settings.widgetWindowKind.name\n prefs[Keys.CODEX_USAGE_DISPLAY] = settings.usageDisplayMode.name\n prefs[Keys.CODEX_USAGE_PROGRESS_STYLE] = settings.usageProgressStyle.name\n }\n }\n\n override suspend fun clearCodexSettings() {\n val current = getCodexSettings()\n saveCodexSettings(\n CodexSettings(\n widgetWindowKind = current.widgetWindowKind,\n usageDisplayMode = current.usageDisplayMode,\n usageProgressStyle = current.usageProgressStyle,\n ),\n )\n }\n\n override fun observeNewApiSettings(): Flow<NewApiSettings> =\n dataStore.data.map { prefs -> prefs.toNewApiSettings() }\n\n override suspend fun getNewApiSettings(): NewApiSettings =\n dataStore.data.first().toNewApiSettings()\n\n override suspend fun saveNewApiSettings(settings: NewApiSettings) {\n dataStore.edit { prefs ->\n if (!settings.isConfigured) {\n prefs.remove(Keys.NEW_API_BASE_URL)\n prefs.remove(Keys.NEW_API_KEY_ENC)\n prefs.clearWidgetPayload(PlatformIds.NEW_API)\n prefs[widgetStatusKey(PlatformIds.NEW_API)] = Status.NOT_CONFIGURED\n } else {\n prefs[Keys.NEW_API_BASE_URL] = settings.baseUrl.trim().trimEnd('/')\n prefs[Keys.NEW_API_KEY_ENC] = encrypt(settings.apiKey, NEW_API_KEY_AD)\n val status = prefs[widgetStatusKey(PlatformIds.NEW_API)]\n if (status == null ||\n status == Status.NOT_CONFIGURED ||\n status == Status.NEEDS_REAUTH\n ) {\n prefs[widgetStatusKey(PlatformIds.NEW_API)] = Status.LOADING\n }\n }\n prefs[Keys.NEW_API_USAGE_DISPLAY] = settings.usageDisplayMode.name\n prefs[Keys.NEW_API_USAGE_PROGRESS_STYLE] = settings.usageProgressStyle.name\n prefs[Keys.NEW_API_QUOTA_PER_USD] = settings.quotaPerUsd.takeIf { it > 0L }\n ?: DEFAULT_NEW_API_QUOTA_PER_USD\n }\n }\n\n override suspend fun clearNewApiSettings() {\n val current = getNewApiSettings()\n saveNewApiSettings(\n NewApiSettings(\n quotaPerUsd = current.quotaPerUsd,\n usageDisplayMode = current.usageDisplayMode,\n usageProgressStyle = current.usageProgressStyle,\n ),\n )\n }\n\n override fun observeWidgetState(platformId: String): Flow<WidgetDisplayState> =\n dataStore.data.map { prefs -> prefs.toWidgetState(platformId) }\n\n override suspend fun getWidgetState(platformId: String): WidgetDisplayState {\n ensureLegacyMigrated()\n return dataStore.data.first().toWidgetState(platformId)\n }\n\n private suspend fun ensureLegacyMigrated() {\n dataStore.edit { it.migrateLegacyWidgetIfNeeded() }\n }\n\n override suspend fun saveWidgetSuccess(platformId: String, snapshot: QuotaSnapshot) {\n dataStore.edit { prefs ->\n prefs.migrateLegacyWidgetIfNeeded()\n prefs[widgetStatusKey(platformId)] = Status.SUCCESS\n prefs[widgetPlatformIdKey(platformId)] = snapshot.platformId\n prefs[widgetPlatformNameKey(platformId)] = snapshot.platformName\n prefs[widgetCurrencyKey(platformId)] = snapshot.currency.name\n prefs[widgetTotalKey(platformId)] = snapshot.totalBalance\n prefs[widgetFormattedKey(platformId)] = snapshot.primaryDisplay\n prefs[widgetUpdatedAtKey(platformId)] = snapshot.updatedAtEpochMs\n prefs[widgetWindowsKey(platformId)] = encodeQuotaWindows(snapshot.windows)\n prefs[widgetUnlimitedKey(platformId)] = snapshot.unlimitedQuota\n prefs[widgetEmptyLimitedQuotaKey(platformId)] = snapshot.emptyLimitedQuota\n prefs[widgetTokenExpiredKey(platformId)] = snapshot.tokenExpired\n prefs[widgetQuotaOverspentKey(platformId)] = snapshot.quotaOverspent\n if (snapshot.usedDisplay.isBlank()) {\n prefs.remove(widgetUsedDisplayKey(platformId))\n } else {\n prefs[widgetUsedDisplayKey(platformId)] = snapshot.usedDisplay\n }\n prefs.remove(widgetErrorKey(platformId))\n }\n }\n\n override suspend fun saveWidgetError(platformId: String, message: String) {\n dataStore.edit { prefs ->\n prefs.migrateLegacyWidgetIfNeeded()\n prefs[widgetStatusKey(platformId)] = Status.ERROR\n prefs[widgetErrorKey(platformId)] = message\n }\n }\n\n override suspend fun saveWidgetLoading(platformId: String) {\n dataStore.edit { prefs ->\n prefs.migrateLegacyWidgetIfNeeded()\n prefs[widgetStatusKey(platformId)] = Status.LOADING\n prefs.remove(widgetErrorKey(platformId))\n }\n }\n\n override suspend fun saveWidgetNotConfigured(platformId: String) {\n dataStore.edit { prefs ->\n prefs.migrateLegacyWidgetIfNeeded()\n prefs[widgetStatusKey(platformId)] = Status.NOT_CONFIGURED\n prefs.clearWidgetPayload(platformId)\n }\n }\n\n override suspend fun saveWidgetNeedsReauth(platformId: String) {\n dataStore.edit { prefs ->\n prefs.migrateLegacyWidgetIfNeeded()\n prefs[widgetStatusKey(platformId)] = Status.NEEDS_REAUTH\n prefs.remove(widgetErrorKey(platformId))\n }\n }\n\n override suspend fun getRefreshIconPhase(platformId: String): RefreshIconPhase {\n val prefs = dataStore.data.first()\n return RefreshIconPhase.fromStorage(\n prefs[refreshPhaseKey(platformId)] ?: prefs[Keys.LEGACY_REFRESH_ICON_PHASE],\n )\n }\n\n override suspend fun setRefreshIconPhase(platformId: String, phase: RefreshIconPhase) {\n dataStore.edit { prefs ->\n prefs[refreshPhaseKey(platformId)] = phase.name\n prefs.remove(Keys.LEGACY_REFRESH_ICON_PHASE)\n }\n }\n\n override suspend fun getRefreshStartedAtEpochMs(platformId: String): Long {\n val prefs = dataStore.data.first()\n return prefs[refreshStartedAtKey(platformId)]\n ?: prefs[Keys.LEGACY_REFRESH_STARTED_AT]\n ?: 0L\n }\n\n override suspend fun setRefreshStartedAtEpochMs(platformId: String, epochMs: Long) {\n dataStore.edit { prefs ->\n prefs[refreshStartedAtKey(platformId)] = epochMs\n prefs.remove(Keys.LEGACY_REFRESH_STARTED_AT)\n }\n }\n\n override suspend fun clearAllRefreshIconPhases() {\n dataStore.edit { prefs ->\n for (platformId in listOf(\n PlatformIds.DEEPSEEK,\n PlatformIds.OPENCODE_GO,\n PlatformIds.CODEX,\n PlatformIds.NEW_API,\n )) {\n prefs[refreshPhaseKey(platformId)] = RefreshIconPhase.Idle.name\n }\n prefs.remove(Keys.LEGACY_REFRESH_ICON_PHASE)\n }\n }\n\n override suspend fun isPlatformTipDismissed(): Boolean =\n dataStore.data.first()[Keys.PLATFORM_TIP_DISMISSED] == true\n\n override suspend fun setPlatformTipDismissed(dismissed: Boolean) {\n dataStore.edit { prefs ->\n prefs[Keys.PLATFORM_TIP_DISMISSED] = dismissed\n }\n }\n\n override suspend fun isOemBackgroundTipDismissed(): Boolean =\n dataStore.data.first()[Keys.OEM_BACKGROUND_TIP_DISMISSED] == true\n\n override suspend fun setOemBackgroundTipDismissed(dismissed: Boolean) {\n dataStore.edit { prefs ->\n prefs[Keys.OEM_BACKGROUND_TIP_DISMISSED] = dismissed\n }\n }\n\n override suspend fun getUpdateIgnoredVersion(): String? =\n dataStore.data.first()[Keys.UPDATE_IGNORED_VERSION]\n\n override suspend fun setUpdateIgnoredVersion(version: String?) {\n dataStore.edit { prefs ->\n if (version.isNullOrBlank()) {\n prefs.remove(Keys.UPDATE_IGNORED_VERSION)\n } else {\n prefs[Keys.UPDATE_IGNORED_VERSION] = version\n }\n }\n }\n\n override fun observeAppSettings(): Flow<AppSettings> =\n dataStore.data.map { prefs -> prefs.toAppSettings() }\n\n override suspend fun getAppSettings(): AppSettings =\n dataStore.data.first().toAppSettings()\n\n override suspend fun saveAppSettings(settings: AppSettings) {\n dataStore.edit { prefs ->\n prefs[Keys.DARK_THEME_MODE] = settings.darkThemeMode.name\n prefs[Keys.THEME_COLOR_MODE] = settings.themeColorMode.name\n prefs[Keys.CUSTOM_SEED_COLOR_ARGB] = settings.customSeedColorArgb\n prefs[Keys.REFRESH_INTERVAL_MINUTES] = settings.refreshIntervalMinutes\n prefs[Keys.CHECK_FOR_UPDATES_ON_LAUNCH] = settings.checkForUpdatesOnLaunch\n }\n }\n\n private fun Preferences.toAppSettings(): AppSettings {\n val interval = this[Keys.REFRESH_INTERVAL_MINUTES] ?: DEFAULT_REFRESH_INTERVAL_MINUTES\n return AppSettings(\n darkThemeMode = DarkThemeMode.fromStorage(this[Keys.DARK_THEME_MODE]),\n themeColorMode = ThemeColorMode.fromStorage(this[Keys.THEME_COLOR_MODE]),\n customSeedColorArgb = this[Keys.CUSTOM_SEED_COLOR_ARGB] ?: DEFAULT_CUSTOM_SEED_COLOR_ARGB,\n refreshIntervalMinutes = interval.takeIf { it in ALLOWED_REFRESH_INTERVAL_MINUTES }\n ?: DEFAULT_REFRESH_INTERVAL_MINUTES,\n checkForUpdatesOnLaunch = this[Keys.CHECK_FOR_UPDATES_ON_LAUNCH] ?: true,\n )\n }\n\n private fun Preferences.toDeepSeekSettings(): DeepSeekSettings {\n val encrypted = this[Keys.API_KEY_ENCRYPTED]\n val apiKey = encrypted?.let { runCatching { decrypt(it, API_KEY_AD) }.getOrDefault(\"\") }.orEmpty()\n return DeepSeekSettings(\n apiKey = apiKey,\n currency = CurrencyPreference.fromStorage(this[Keys.CURRENCY]),\n )\n }\n\n private fun Preferences.toOpenCodeGoSettings(): OpenCodeGoSettings {\n val encrypted = this[Keys.OPENCODE_AUTH_COOKIE_ENC]\n val authCookie = encrypted\n ?.let { runCatching { decrypt(it, OPENCODE_AUTH_AD) }.getOrDefault(\"\") }\n .orEmpty()\n return OpenCodeGoSettings(\n workspaceId = this[Keys.OPENCODE_WORKSPACE_ID].orEmpty(),\n workspaceName = this[Keys.OPENCODE_WORKSPACE_NAME].orEmpty(),\n authCookie = authCookie,\n widgetWindowKind = UsageWindowKind.fromStorage(this[Keys.OPENCODE_WIDGET_WINDOW]),\n usageDisplayMode = UsageDisplayMode.fromStorage(this[Keys.OPENCODE_USAGE_DISPLAY]),\n usageProgressStyle = UsageProgressStyle.fromStorage(\n this[Keys.OPENCODE_USAGE_PROGRESS_STYLE],\n ),\n )\n }\n\n private fun Preferences.toCodexSettings(): CodexSettings {\n fun decryptOrEmpty(key: Preferences.Key<String>, ad: ByteArray): String =\n this[key]?.let { runCatching { decrypt(it, ad) }.getOrDefault(\"\") }.orEmpty()\n return CodexSettings(\n accessToken = decryptOrEmpty(Keys.CODEX_ACCESS_ENC, CODEX_ACCESS_AD),\n refreshToken = decryptOrEmpty(Keys.CODEX_REFRESH_ENC, CODEX_REFRESH_AD),\n idToken = decryptOrEmpty(Keys.CODEX_ID_ENC, CODEX_ID_AD),\n accountId = this[Keys.CODEX_ACCOUNT_ID].orEmpty(),\n expiresAtEpochMs = this[Keys.CODEX_EXPIRES_AT] ?: 0L,\n email = this[Keys.CODEX_EMAIL].orEmpty(),\n planType = this[Keys.CODEX_PLAN_TYPE].orEmpty(),\n widgetWindowKind = this[Keys.CODEX_WIDGET_WINDOW]\n ?.let { UsageWindowKind.fromStorage(it) }\n ?: UsageWindowKind.WEEKLY,\n usageDisplayMode = UsageDisplayMode.fromStorage(this[Keys.CODEX_USAGE_DISPLAY]),\n usageProgressStyle = UsageProgressStyle.fromStorage(\n this[Keys.CODEX_USAGE_PROGRESS_STYLE],\n ),\n )\n }\n\n private fun Preferences.toNewApiSettings(): NewApiSettings {\n val encrypted = this[Keys.NEW_API_KEY_ENC]\n val apiKey = encrypted\n ?.let { runCatching { decrypt(it, NEW_API_KEY_AD) }.getOrDefault(\"\") }\n .orEmpty()\n val quotaPerUsd = this[Keys.NEW_API_QUOTA_PER_USD]\n ?.takeIf { it > 0L }\n ?: DEFAULT_NEW_API_QUOTA_PER_USD\n return NewApiSettings(\n baseUrl = this[Keys.NEW_API_BASE_URL].orEmpty(),\n apiKey = apiKey,\n quotaPerUsd = quotaPerUsd,\n usageDisplayMode = UsageDisplayMode.fromStorage(this[Keys.NEW_API_USAGE_DISPLAY]),\n usageProgressStyle = UsageProgressStyle.fromStorage(\n this[Keys.NEW_API_USAGE_PROGRESS_STYLE],\n ),\n )\n }\n\n private fun Preferences.isConfigured(platformId: String): Boolean =\n when (platformId) {\n PlatformIds.DEEPSEEK -> toDeepSeekSettings().apiKey.isNotBlank()\n PlatformIds.OPENCODE_GO -> toOpenCodeGoSettings().isConfigured\n PlatformIds.CODEX -> toCodexSettings().isConfigured\n PlatformIds.NEW_API -> toNewApiSettings().isConfigured\n else -> false\n }\n\n private fun Preferences.toWidgetState(platformId: String): WidgetDisplayState {\n if (!isConfigured(platformId)) {\n return WidgetDisplayState.NotConfigured\n }\n val status = this[widgetStatusKey(platformId)]\n ?: if (platformId == PlatformIds.DEEPSEEK) this[Keys.LEGACY_WIDGET_STATUS] else null\n return when (status) {\n Status.LOADING -> WidgetDisplayState.Loading\n Status.NEEDS_REAUTH -> WidgetDisplayState.NeedsReauth\n Status.ERROR -> WidgetDisplayState.Error(\n this[widgetErrorKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_ERROR]\n ?: \"刷新失败\",\n )\n Status.SUCCESS -> {\n val storedPlatformId = this[widgetPlatformIdKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_PLATFORM_ID]\n ?: platformId\n val currency = CurrencyPreference.fromStorage(\n this[widgetCurrencyKey(platformId)] ?: this[Keys.LEGACY_WIDGET_CURRENCY],\n )\n val total = this[widgetTotalKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_TOTAL]\n ?: \"0\"\n val primary = this[widgetFormattedKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_FORMATTED]\n ?: if (platformId == PlatformIds.DEEPSEEK) {\n formatBalance(currency, total)\n } else {\n \"\"\n }\n val windows = decodeQuotaWindows(\n this[widgetWindowsKey(platformId)] ?: this[Keys.LEGACY_WIDGET_WINDOWS],\n ) ?: defaultWindows(platformId)\n WidgetDisplayState.Success(\n QuotaSnapshot(\n platformId = storedPlatformId,\n platformName = this[widgetPlatformNameKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_PLATFORM_NAME]\n ?: PlatformRegistry.displayName(platformId),\n windows = windows,\n primaryDisplay = primary,\n updatedAtEpochMs = this[widgetUpdatedAtKey(platformId)]\n ?: this[Keys.LEGACY_WIDGET_UPDATED_AT]\n ?: 0L,\n currency = currency,\n totalBalance = total,\n unlimitedQuota = this[widgetUnlimitedKey(platformId)] == true,\n usedDisplay = this[widgetUsedDisplayKey(platformId)].orEmpty(),\n emptyLimitedQuota = this[widgetEmptyLimitedQuotaKey(platformId)] == true,\n tokenExpired = this[widgetTokenExpiredKey(platformId)] == true,\n quotaOverspent = this[widgetQuotaOverspentKey(platformId)] == true,\n ),\n )\n }\n else -> WidgetDisplayState.Loading\n }\n }\n\n private fun defaultWindows(platformId: String): List<QuotaWindow> =\n when (platformId) {\n PlatformIds.DEEPSEEK -> listOf(QuotaWindow(kind = QuotaWindowKind.BALANCE))\n PlatformIds.NEW_API -> listOf(\n QuotaWindow(kind = QuotaWindowKind.BALANCE),\n QuotaWindow(kind = QuotaWindowKind.TOKEN),\n )\n else -> emptyList()\n }\n\n /**\n * One-time migration: copy legacy global `widget_*` keys into DeepSeek-prefixed keys.\n */\n private fun MutablePreferences.migrateLegacyWidgetIfNeeded() {\n if (this[widgetStatusKey(PlatformIds.DEEPSEEK)] != null) return\n val legacyStatus = this[Keys.LEGACY_WIDGET_STATUS] ?: return\n this[widgetStatusKey(PlatformIds.DEEPSEEK)] = legacyStatus\n this[Keys.LEGACY_WIDGET_ERROR]?.let { this[widgetErrorKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_PLATFORM_ID]?.let { this[widgetPlatformIdKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_PLATFORM_NAME]?.let { this[widgetPlatformNameKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_CURRENCY]?.let { this[widgetCurrencyKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_TOTAL]?.let { this[widgetTotalKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_FORMATTED]?.let { this[widgetFormattedKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_WINDOWS]?.let { this[widgetWindowsKey(PlatformIds.DEEPSEEK)] = it }\n this[Keys.LEGACY_WIDGET_UPDATED_AT]?.let { this[widgetUpdatedAtKey(PlatformIds.DEEPSEEK)] = it }\n remove(Keys.LEGACY_WIDGET_STATUS)\n remove(Keys.LEGACY_WIDGET_ERROR)\n remove(Keys.LEGACY_WIDGET_PLATFORM_ID)\n remove(Keys.LEGACY_WIDGET_PLATFORM_NAME)\n remove(Keys.LEGACY_WIDGET_CURRENCY)\n remove(Keys.LEGACY_WIDGET_TOTAL)\n remove(Keys.LEGACY_WIDGET_FORMATTED)\n remove(Keys.LEGACY_WIDGET_WINDOWS)\n remove(Keys.LEGACY_WIDGET_UPDATED_AT)\n remove(Keys.ACTIVE_PLATFORM_ID)\n }\n\n private fun MutablePreferences.clearWidgetPayload(platformId: String) {\n remove(widgetErrorKey(platformId))\n remove(widgetFormattedKey(platformId))\n remove(widgetWindowsKey(platformId))\n remove(widgetPlatformIdKey(platformId))\n remove(widgetPlatformNameKey(platformId))\n remove(widgetCurrencyKey(platformId))\n remove(widgetTotalKey(platformId))\n remove(widgetUpdatedAtKey(platformId))\n remove(widgetUnlimitedKey(platformId))\n remove(widgetUsedDisplayKey(platformId))\n remove(widgetEmptyLimitedQuotaKey(platformId))\n remove(widgetTokenExpiredKey(platformId))\n remove(widgetQuotaOverspentKey(platformId))\n }\n\n private fun encrypt(plain: String, associatedData: ByteArray): String {\n val cipher = aead.encrypt(plain.encodeToByteArray(), associatedData)\n return Base64.encodeToString(cipher, Base64.NO_WRAP)\n }\n\n private fun decrypt(encoded: String, associatedData: ByteArray): String {\n val cipher = Base64.decode(encoded, Base64.NO_WRAP)\n return aead.decrypt(cipher, associatedData).decodeToString()\n }\n\n private object Keys {\n val API_KEY_ENCRYPTED = stringPreferencesKey(\"deepseek_api_key_enc\")\n val CURRENCY = stringPreferencesKey(\"deepseek_currency\")\n val OPENCODE_WORKSPACE_ID = stringPreferencesKey(\"opencode_workspace_id\")\n val OPENCODE_WORKSPACE_NAME = stringPreferencesKey(\"opencode_workspace_name\")\n val OPENCODE_AUTH_COOKIE_ENC = stringPreferencesKey(\"opencode_auth_cookie_enc\")\n val OPENCODE_WIDGET_WINDOW = stringPreferencesKey(\"opencode_go_widget_window\")\n val OPENCODE_USAGE_DISPLAY = stringPreferencesKey(\"opencode_go_usage_display\")\n val OPENCODE_USAGE_PROGRESS_STYLE =\n stringPreferencesKey(\"opencode_go_usage_progress_style\")\n val CODEX_ACCESS_ENC = stringPreferencesKey(\"codex_access_token_enc\")\n val CODEX_REFRESH_ENC = stringPreferencesKey(\"codex_refresh_token_enc\")\n val CODEX_ID_ENC = stringPreferencesKey(\"codex_id_token_enc\")\n val CODEX_ACCOUNT_ID = stringPreferencesKey(\"codex_account_id\")\n val CODEX_EXPIRES_AT = longPreferencesKey(\"codex_expires_at\")\n val CODEX_EMAIL = stringPreferencesKey(\"codex_email\")\n val CODEX_PLAN_TYPE = stringPreferencesKey(\"codex_plan_type\")\n val CODEX_WIDGET_WINDOW = stringPreferencesKey(\"codex_widget_window\")\n val CODEX_USAGE_DISPLAY = stringPreferencesKey(\"codex_usage_display\")\n val CODEX_USAGE_PROGRESS_STYLE = stringPreferencesKey(\"codex_usage_progress_style\")\n val NEW_API_BASE_URL = stringPreferencesKey(\"new_api_base_url\")\n val NEW_API_KEY_ENC = stringPreferencesKey(\"new_api_key_enc\")\n val NEW_API_QUOTA_PER_USD = longPreferencesKey(\"new_api_quota_per_usd\")\n val NEW_API_USAGE_DISPLAY = stringPreferencesKey(\"new_api_usage_display\")\n val NEW_API_USAGE_PROGRESS_STYLE = stringPreferencesKey(\"new_api_usage_progress_style\")\n val ACTIVE_PLATFORM_ID = stringPreferencesKey(\"active_platform_id\")\n val LEGACY_WIDGET_STATUS = stringPreferencesKey(\"widget_status\")\n val LEGACY_WIDGET_ERROR = stringPreferencesKey(\"widget_error\")\n val LEGACY_WIDGET_PLATFORM_ID = stringPreferencesKey(\"widget_platform_id\")\n val LEGACY_WIDGET_PLATFORM_NAME = stringPreferencesKey(\"widget_platform_name\")\n val LEGACY_WIDGET_CURRENCY = stringPreferencesKey(\"widget_currency\")\n val LEGACY_WIDGET_TOTAL = stringPreferencesKey(\"widget_total\")\n val LEGACY_WIDGET_FORMATTED = stringPreferencesKey(\"widget_formatted\")\n val LEGACY_WIDGET_WINDOWS = stringPreferencesKey(\"widget_windows\")\n val LEGACY_WIDGET_UPDATED_AT = longPreferencesKey(\"widget_updated_at\")\n val LEGACY_REFRESH_ICON_PHASE = stringPreferencesKey(\"refresh_icon_phase\")\n val LEGACY_REFRESH_STARTED_AT = longPreferencesKey(\"refresh_started_at\")\n val PLATFORM_TIP_DISMISSED = booleanPreferencesKey(\"platform_tip_dismissed\")\n val OEM_BACKGROUND_TIP_DISMISSED = booleanPreferencesKey(\"oem_background_tip_dismissed\")\n val DARK_THEME_MODE = stringPreferencesKey(\"dark_theme_mode\")\n val THEME_COLOR_MODE = stringPreferencesKey(\"theme_color_mode\")\n val CUSTOM_SEED_COLOR_ARGB = intPreferencesKey(\"custom_seed_color_argb\")\n val REFRESH_INTERVAL_MINUTES = intPreferencesKey(\"refresh_interval_minutes\")\n val CHECK_FOR_UPDATES_ON_LAUNCH = booleanPreferencesKey(\"check_for_updates_on_launch\")\n val UPDATE_IGNORED_VERSION = stringPreferencesKey(\"update_ignored_version\")\n }\n\n private object Status {\n const val NOT_CONFIGURED = \"not_configured\"\n const val LOADING = \"loading\"\n const val SUCCESS = \"success\"\n const val ERROR = \"error\"\n const val NEEDS_REAUTH = \"needs_reauth\"\n }\n\n companion object {\n private val API_KEY_AD = \"deepseek_api_key\".encodeToByteArray()\n private val OPENCODE_AUTH_AD = \"opencode_auth_cookie\".encodeToByteArray()\n private val CODEX_ACCESS_AD = \"codex_access_token\".encodeToByteArray()\n private val CODEX_REFRESH_AD = \"codex_refresh_token\".encodeToByteArray()\n private val CODEX_ID_AD = \"codex_id_token\".encodeToByteArray()\n private val NEW_API_KEY_AD = \"new_api_key\".encodeToByteArray()\n\n private fun widgetStatusKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_status\")\n\n private fun widgetErrorKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_error\")\n\n private fun widgetPlatformIdKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_platform_id\")\n\n private fun widgetPlatformNameKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_platform_name\")\n\n private fun widgetCurrencyKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_currency\")\n\n private fun widgetTotalKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_total\")\n\n private fun widgetFormattedKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_formatted\")\n\n private fun widgetWindowsKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_windows\")\n\n private fun widgetUpdatedAtKey(platformId: String) =\n longPreferencesKey(\"widget_${platformId}_updated_at\")\n\n private fun widgetUnlimitedKey(platformId: String) =\n booleanPreferencesKey(\"widget_${platformId}_unlimited\")\n\n private fun widgetUsedDisplayKey(platformId: String) =\n stringPreferencesKey(\"widget_${platformId}_used_display\")\n\n private fun widgetEmptyLimitedQuotaKey(platformId: String) =\n booleanPreferencesKey(\"widget_${platformId}_empty_limited_quota\")\n\n private fun widgetTokenExpiredKey(platformId: String) =\n booleanPreferencesKey(\"widget_${platformId}_token_expired\")\n\n private fun widgetQuotaOverspentKey(platformId: String) =\n booleanPreferencesKey(\"widget_${platformId}_quota_overspent\")\n\n private fun refreshPhaseKey(platformId: String) =\n stringPreferencesKey(\"refresh_${platformId}_icon_phase\")\n\n private fun refreshStartedAtKey(platformId: String) =\n longPreferencesKey(\"refresh_${platformId}_started_at\")\n\n private fun createAead(context: Context): Aead {\n AeadConfig.register()\n return AndroidKeysetManager.Builder()\n .withSharedPref(context, \"quota_widget_tink_keyset\", \"quota_widget_tink_prefs\")\n .withKeyTemplate(KeyTemplates.get(\"AES256_GCM\"))\n .withMasterKeyUri(\"android-keystore://quota_widget_master_key\")\n .build()\n .keysetHandle\n .getPrimitive(Aead::class.java)\n }\n }\n}\n"}}}}