Compare commits

...

35 Commits

Author SHA1 Message Date
618eecf676 Merge pull request '_tmp_update' (#11) from _tmp_update into dev 2026-10-10 02:39:06 +08:00
8207285b23 update 1.7.22 final2
Some checks are pending
CI / Unit Tests & Lint (pull_request) Waiting to run
CI / Debug APK (pull_request) Blocked by required conditions
CI / Release APK (debug-signed) (pull_request) Blocked by required conditions
2026-10-10 02:39:01 +08:00
82bd04597d Merge pull request '_tmp_update' (#3) from _tmp_update into dev 2026-10-10 02:00:16 +08:00
5850fc452c update 1.7.22 final
Some checks are pending
CI / Unit Tests & Lint (pull_request) Waiting to run
CI / Debug APK (pull_request) Blocked by required conditions
CI / Release APK (debug-signed) (pull_request) Blocked by required conditions
2026-10-10 02:00:15 +08:00
c6a75f8fce Merge pull request '_tmp_update' (#2) from _tmp_update into dev 2026-10-10 01:47:32 +08:00
a384309689 update 1.7.22
Some checks are pending
CI / Unit Tests & Lint (pull_request) Waiting to run
CI / Debug APK (pull_request) Blocked by required conditions
CI / Release APK (debug-signed) (pull_request) Blocked by required conditions
2026-10-10 01:47:31 +08:00
8692014340 Merge pull request 'chore(update): publish 1.7.21 manifest' (#1) from _tmp_update into dev
Some checks are pending
Release / build (push) Waiting to run
2026-10-10 00:25:54 +08:00
15c6bc24c4 chore(update): 发布 1.7.21 更新清单
Some checks are pending
CI / Unit Tests & Lint (pull_request) Waiting to run
CI / Debug APK (pull_request) Blocked by required conditions
CI / Release APK (debug-signed) (pull_request) Blocked by required conditions
2026-10-10 00:25:12 +08:00
460bed7279 chore(update): 发布 1.7.20 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 23:16:10 +08:00
ecbc86fdf2 chore(update): 发布 1.7.19 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 22:43:53 +08:00
05b5107f46 chore(update): 发布 1.7.18 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 22:32:24 +08:00
bccd7bad83 chore(update): 发布 1.7.17 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 22:14:31 +08:00
c5120c5952 chore(update): 发布 1.7.16 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 18:39:17 +08:00
2c5a3b2055 chore(update): 发布 1.7.15 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 18:02:12 +08:00
c76193e892 chore(update): 发布 1.7.14 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 17:22:54 +08:00
6edd68f80e chore(update): 发布 1.7.13 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 17:12:43 +08:00
750b9d9f90 chore(update): 发布 1.7.12 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 16:42:53 +08:00
0f4d68033e chore(update): 发布 1.7.11 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 16:33:50 +08:00
7c7a23f097 chore(update): 发布 1.7.10 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 15:36:25 +08:00
7c3809e879 chore(update): 发布 1.7.9 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 15:29:08 +08:00
7ad49e994d chore(update): 发布 1.7.8 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 12:27:18 +08:00
8f40360b9f chore(update): 发布 1.7.7 更新清单
Some checks are pending
Release / build (push) Waiting to run
2026-10-09 11:51:52 +08:00
08a3b42a82 chore(update): 发布 1.7.6 更新清单 2026-10-09 11:23:01 +08:00
aab50bbe78 chore(update): 发布 1.7.5 更新清单 2026-10-09 11:04:35 +08:00
faf7f40524 chore(update): 发布 1.7.4 更新清单 2026-10-09 10:47:27 +08:00
342788056b chore(update): 发布 1.7.3 更新清单 2026-10-09 10:29:48 +08:00
b479cb2a7a chore(update): 发布 1.7.2 更新清单 2026-10-09 10:22:57 +08:00
6e0d67afb7 chore(update): 发布 1.7.1 更新清单 2026-10-09 10:13:03 +08:00
8d68c3b280 chore(update): 发布 1.7.0 更新清单 2026-10-09 10:03:32 +08:00
71f3f8d554 chore(update): 发布 1.6.9 更新清单 2026-10-09 09:47:13 +08:00
dfd91a5355 chore(update): 发布 1.6.8 更新清单 2026-10-09 09:40:01 +08:00
149fc5f125 chore(update): 发布 1.6.7 更新清单 2026-10-09 09:31:01 +08:00
95cde916ca chore(update): 发布 1.6.6 更新清单 2026-10-09 09:18:15 +08:00
79f99f5dee fix(credential): 多账号会话凭据按请求账号精确匹配;UI微调
- CredentialRepository 快照新增 requestedAccountId,暂存凭据严格按请求账号匹配,
  避免 A 账号刷新期间把暂存凭据误交给默认账号/其他账号
- Dashboard 首页提示与自动签到开关改为卡片样式,开关配色对齐雨晴粉主题
- 默认账号徽标加 maxLines/softWrap 防窄屏换行
2026-10-08 22:16:06 +08:00
2509ab8a85 feat(update): 设置页新增检测更新/下载APK/自动安装;接入Gitea update.json清单 2026-10-08 21:42:45 +08:00
14 changed files with 479 additions and 26 deletions

View File

@ -76,6 +76,16 @@
android:name="miuiWidgetVersion" android:name="miuiWidgetVersion"
android:value="1" /> android:value="1" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -40,7 +40,9 @@ class CredentialRepository @Inject constructor(
val revision: Long, val revision: Long,
val fingerprint: String, val fingerprint: String,
/** 快照所属账号;单账号服务为 null。 */ /** 快照所属账号;单账号服务为 null。 */
val accountId: String? = null val accountId: String? = null,
/** 创建快照时调用方请求的账号 ID(多账号服务可能是 null=默认账号),用于会话内精确匹配。 */
val requestedAccountId: String? = accountId
) { ) {
val service: ServiceType get() = credential.service val service: ServiceType get() = credential.service
} }
@ -237,9 +239,9 @@ class CredentialRepository @Inject constructor(
if (session.snapshot.service == service) { if (session.snapshot.service == service) {
val matches = if (ServiceType.isMultiAccount(service)) { val matches = if (ServiceType.isMultiAccount(service)) {
val requested = accountId?.takeIf { it.isNotBlank() } val requested = accountId?.takeIf { it.isNotBlank() }
requested == null || // 只允许读取与快照同一账号的暂存凭据;null=默认账号也要看快照当初请求的是不是默认账号,
session.snapshot.accountId == null || // 避免 A 账号刷新期间把 A 的暂存凭据误交给默认账号(B)的读请求。
session.snapshot.accountId == requested requested == session.snapshot.requestedAccountId
} else { } else {
true true
} }
@ -368,7 +370,8 @@ class CredentialRepository @Inject constructor(
credential = credential, credential = credential,
revision = revisionFor(service, resolvedId), revision = revisionFor(service, resolvedId),
fingerprint = credentialFingerprint(credential)!!, fingerprint = credentialFingerprint(credential)!!,
accountId = resolvedId accountId = resolvedId,
requestedAccountId = accountId?.takeIf { it.isNotBlank() }
) )
} }

View File

@ -0,0 +1,112 @@
package com.rainy.token.data.repository
import android.content.Context
import com.rainy.token.BuildConfig
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
/**
* 应用更新仓库:从 Gitea 仓库的 update.json 读取版本清单,
* 下载 APK 到应用缓存目录供 FileProvider 安装。
*/
@Singleton
class UpdateRepository @Inject constructor(
private val okHttpClient: OkHttpClient,
@ApplicationContext context: Context
) {
private val appContext = context.applicationContext
private val json = Json { ignoreUnknownKeys = true }
/** 检查是否有新版本。null = 已是最新;非空 = 远端有更新。 */
suspend fun checkForUpdate(): Result<UpdateInfo?> = withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url(UPDATE_MANIFEST_URL)
.get()
.build()
val text = okHttpClient.newCall(request).execute().use { resp ->
if (!resp.isSuccessful) {
return@withContext Result.failure(IOException("HTTP " + resp.code))
}
resp.body?.string().orEmpty()
}
val manifest = json.decodeFromString<UpdateManifest>(text)
val hasUpdate = manifest.versionCode > BuildConfig.VERSION_CODE
Result.success(if (hasUpdate) {
UpdateInfo(
versionCode = manifest.versionCode,
versionName = manifest.versionName,
apkUrl = manifest.apkUrl,
notes = manifest.notes
)
} else {
null
})
} catch (e: Exception) {
Result.failure(e)
}
}
/** 下载 APK 到缓存目录。onProgress 回调下载字节数/总字节数(total 可能为 -1)。 */
suspend fun downloadApk(
info: UpdateInfo,
onProgress: (downloaded: Long, total: Long) -> Unit
): Result<File> = withContext(Dispatchers.IO) {
try {
val dir = File(appContext.cacheDir, "apk_downloads").apply { mkdirs() }
val file = File(dir, "rainytoken-" + info.versionName + ".apk")
val request = Request.Builder().url(info.apkUrl).get().build()
okHttpClient.newCall(request).execute().use { resp ->
if (!resp.isSuccessful) {
return@withContext Result.failure(IOException("HTTP " + resp.code))
}
val body = resp.body ?: return@withContext Result.failure(IOException("empty body"))
val total = body.contentLength()
file.outputStream().buffered().use { output ->
body.byteStream().use { input ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var downloaded = 0L
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
downloaded += read
onProgress(downloaded, total)
}
}
}
}
Result.success(file)
} catch (e: Exception) {
Result.failure(e)
}
}
@Serializable
data class UpdateManifest(
val versionCode: Int,
val versionName: String,
val apkUrl: String,
val notes: String = ""
)
data class UpdateInfo(
val versionCode: Int,
val versionName: String,
val apkUrl: String,
val notes: String
)
companion object {
const val UPDATE_MANIFEST_URL =
"http://124.222.97.212:8443/maya/Rainytoken/raw/branch/dev/update.json"
}
}

View File

@ -19,6 +19,7 @@ import com.rainy.token.data.repository.OpenCodeUsageRepository
import com.rainy.token.data.repository.OllamaRepository import com.rainy.token.data.repository.OllamaRepository
import com.rainy.token.data.repository.Sub2ApiRepository import com.rainy.token.data.repository.Sub2ApiRepository
import com.rainy.token.data.repository.TraeRepository import com.rainy.token.data.repository.TraeRepository
import com.rainy.token.data.repository.UpdateRepository
import com.rainy.token.data.repository.WorkBuddyRepository import com.rainy.token.data.repository.WorkBuddyRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -204,6 +205,16 @@ object NetworkModule {
/** /**
* Sub2API 仓库:自托管实例(凭据里的 baseUrl)+ 管理面板 API。 * Sub2API 仓库:自托管实例(凭据里的 baseUrl)+ 管理面板 API。
*/ */
/**
* 更新仓库:读取 Gitea 上的 update.json 并下载 APK。
*/
@Provides
@Singleton
fun provideUpdateRepository(
okHttpClient: OkHttpClient,
@ApplicationContext context: Context
): UpdateRepository = UpdateRepository(okHttpClient, context)
@Provides @Provides
@Singleton @Singleton
fun provideSub2ApiRepository( fun provideSub2ApiRepository(

View File

@ -102,6 +102,8 @@ import com.rainy.token.ui.components.AppTips
import com.rainy.token.ui.components.asString import com.rainy.token.ui.components.asString
import com.rainy.token.ui.dashboard.autocheckin.AutoCheckinManager import com.rainy.token.ui.dashboard.autocheckin.AutoCheckinManager
import com.rainy.token.ui.theme.inkMuted import com.rainy.token.ui.theme.inkMuted
import com.rainy.token.ui.theme.PureWhite
import com.rainy.token.ui.theme.inkWarm
import com.rainy.token.ui.theme.StrawberryPink import com.rainy.token.ui.theme.StrawberryPink
import android.app.AppOpsManager import android.app.AppOpsManager
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
@ -347,9 +349,17 @@ fun DashboardScreen(
// 随机小技巧提示(每次启动随机一条)+ 最右侧自动签到开关 // 随机小技巧提示(每次启动随机一条)+ 最右侧自动签到开关
val tipTextRes = remember { AppTips.randomHintRes() } val tipTextRes = remember { AppTips.randomHintRes() }
val autoCheckinEnabled by viewModel.autoCheckinEnabled.collectAsStateWithLifecycle() val autoCheckinEnabled by viewModel.autoCheckinEnabled.collectAsStateWithLifecycle()
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp) modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)
) { ) {
Text( Text(
text = "💡 " + stringResource(tipTextRes), text = "💡 " + stringResource(tipTextRes),
@ -360,15 +370,24 @@ fun DashboardScreen(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(R.string.auto_checkin_switch_label), text = stringResource(R.string.auto_checkin_switch_label),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelMedium,
color = inkMuted() color = inkWarm(),
fontWeight = FontWeight.SemiBold
) )
Spacer(modifier = Modifier.width(2.dp)) Spacer(modifier = Modifier.width(2.dp))
Switch( Switch(
checked = autoCheckinEnabled, checked = autoCheckinEnabled,
onCheckedChange = viewModel::setAutoCheckinEnabled, onCheckedChange = viewModel::setAutoCheckinEnabled,
colors = SwitchDefaults.colors(checkedThumbColor = StrawberryPink) colors = SwitchDefaults.colors(
checkedThumbColor = PureWhite,
checkedTrackColor = StrawberryPink,
checkedBorderColor = StrawberryPink,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant
) )
)
}
} }
val items = rememberDashboardItems( val items = rememberDashboardItems(
cards = uiState.cards, cards = uiState.cards,
@ -922,6 +941,8 @@ private fun DashboardCard(
text = stringResource(R.string.account_default_badge), text = stringResource(R.string.account_default_badge),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = StrawberryPink, color = StrawberryPink,
maxLines = 1,
softWrap = false,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(StrawberryPink.copy(alpha = 0.12f)) .background(StrawberryPink.copy(alpha = 0.12f))

View File

@ -296,6 +296,8 @@ private fun AccountRow(
text = stringResource(R.string.account_default_badge), text = stringResource(R.string.account_default_badge),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = StrawberryPink, color = StrawberryPink,
maxLines = 1,
softWrap = false,
modifier = Modifier modifier = Modifier
.padding(horizontal = 6.dp, vertical = 2.dp) .padding(horizontal = 6.dp, vertical = 2.dp)
.background( .background(

View File

@ -54,6 +54,11 @@ import com.rainy.token.ui.components.ServiceIcon
import com.rainy.token.ui.components.StatusChip import com.rainy.token.ui.components.StatusChip
import com.rainy.token.ui.components.StatusLevel import com.rainy.token.ui.components.StatusLevel
import com.rainy.token.ui.components.StatusStyle import com.rainy.token.ui.components.StatusStyle
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import com.rainy.token.R import com.rainy.token.R
import com.rainy.token.ui.components.AppTips import com.rainy.token.ui.components.AppTips
import com.rainy.token.ui.theme.InkMuted import com.rainy.token.ui.theme.InkMuted
@ -160,6 +165,9 @@ fun SettingsScreen(
item { item {
DebugLogCard(onClick = { onOpenDebugLog() }) DebugLogCard(onClick = { onOpenDebugLog() })
} }
item {
UpdateCard()
}
item { item {
Spacer(modifier = Modifier.padding(top = 8.dp)) Spacer(modifier = Modifier.padding(top = 8.dp))
AboutCard() AboutCard()
@ -310,6 +318,142 @@ private fun CredentialStatusCard(
} }
} }
@Composable
private fun UpdateCard(
viewModel: UpdateViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = stringResource(R.string.title_update),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.padding(top = 8.dp))
Text(
text = stringResource(
R.string.update_current_version,
state.currentVersionName,
state.currentVersionCode
),
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
)
Spacer(modifier = Modifier.padding(top = 12.dp))
when (val phase = state.phase) {
is UpdateViewModel.Phase.Idle -> Unit
is UpdateViewModel.Phase.Checking -> {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(
modifier = Modifier.width(18.dp).height(18.dp),
color = StrawberryPink,
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.update_checking),
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
)
}
}
is UpdateViewModel.Phase.Latest -> {
Text(
text = stringResource(R.string.update_latest),
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
)
Spacer(modifier = Modifier.padding(top = 8.dp))
TextButton(onClick = { viewModel.checkForUpdate() }) {
Text(stringResource(R.string.update_check))
}
}
is UpdateViewModel.Phase.Available -> {
Text(
text = stringResource(R.string.update_available, phase.info.versionName),
style = MaterialTheme.typography.bodyMedium,
color = StrawberryPink,
fontWeight = FontWeight.SemiBold
)
if (phase.info.notes.isNotBlank()) {
Spacer(modifier = Modifier.padding(top = 4.dp))
Text(
text = phase.info.notes,
style = MaterialTheme.typography.bodySmall,
color = InkMuted
)
}
Spacer(modifier = Modifier.padding(top = 12.dp))
Button(onClick = { viewModel.downloadAndInstall(phase.info) }) {
Text(stringResource(R.string.update_download))
}
}
is UpdateViewModel.Phase.Downloading -> {
LinearProgressIndicator(
progress = { phase.progress },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.padding(top = 4.dp))
Text(
text = stringResource(
R.string.update_downloading,
(phase.progress * 100).toInt()
),
style = MaterialTheme.typography.bodySmall,
color = InkMuted
)
}
is UpdateViewModel.Phase.Ready -> {
Text(
text = stringResource(R.string.update_download_finished),
style = MaterialTheme.typography.bodyMedium,
color = InkMuted
)
Spacer(modifier = Modifier.padding(top = 8.dp))
Button(onClick = { viewModel.install(phase.file) }) {
Text(stringResource(R.string.update_install))
}
}
is UpdateViewModel.Phase.NeedInstallPermission -> {
Text(
text = stringResource(R.string.update_install_permission_tip),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.padding(top = 8.dp))
Button(onClick = {
val intent = Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:" + context.packageName)
)
runCatching { context.startActivity(intent) }
}) {
Text(stringResource(R.string.update_enable_permission))
}
}
is UpdateViewModel.Phase.Error -> {
Text(
text = stringResource(R.string.update_failed, phase.message),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.padding(top = 8.dp))
TextButton(onClick = { viewModel.checkForUpdate() }) {
Text(stringResource(R.string.update_check))
}
}
}
}
}
}
@Composable @Composable
private fun AboutCard() { private fun AboutCard() {
Card( Card(

View File

@ -0,0 +1,98 @@
package com.rainy.token.ui.settings
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rainy.token.BuildConfig
import com.rainy.token.data.repository.UpdateRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** 设置页更新模块:检查更新 → 下载 APK → 拉起系统安装器。 */
@HiltViewModel
class UpdateViewModel @Inject constructor(
private val updateRepository: UpdateRepository,
@ApplicationContext private val context: Context
) : ViewModel() {
sealed interface Phase {
object Idle : Phase
object Checking : Phase
object Latest : Phase
data class Available(val info: UpdateRepository.UpdateInfo) : Phase
data class Downloading(val progress: Float) : Phase
data class Ready(val file: File) : Phase
object NeedInstallPermission : Phase
data class Error(val message: String) : Phase
}
data class UiState(
val phase: Phase = Phase.Idle,
val currentVersionName: String = BuildConfig.VERSION_NAME,
val currentVersionCode: Int = BuildConfig.VERSION_CODE
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
checkForUpdate()
}
fun checkForUpdate() {
_uiState.update { it.copy(phase = Phase.Checking) }
viewModelScope.launch {
updateRepository.checkForUpdate().fold(
onSuccess = { info ->
_uiState.update {
it.copy(phase = if (info == null) Phase.Latest else Phase.Available(info))
}
},
onFailure = { e ->
_uiState.update { it.copy(phase = Phase.Error(e.message ?: "unknown")) }
}
)
}
}
fun downloadAndInstall(info: UpdateRepository.UpdateInfo) {
_uiState.update { it.copy(phase = Phase.Downloading(0f)) }
viewModelScope.launch {
updateRepository.downloadApk(info) { downloaded, total ->
val progress = if (total > 0) downloaded.toFloat() / total else 0f
_uiState.update { it.copy(phase = Phase.Downloading(progress)) }
}.fold(
onSuccess = { file -> install(file) },
onFailure = { e ->
_uiState.update { it.copy(phase = Phase.Error(e.message ?: "unknown")) }
}
)
}
}
fun install(file: File) {
if (context.packageManager.canRequestPackageInstalls()) {
_uiState.update { it.copy(phase = Phase.Ready(file)) }
val authority = context.packageName + ".fileprovider"
val uri = FileProvider.getUriForFile(context, authority, file)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
}
runCatching { context.startActivity(intent) }.onFailure { e ->
_uiState.update { it.copy(phase = Phase.Error(e.message ?: "cannot start installer")) }
}
} else {
_uiState.update { it.copy(phase = Phase.NeedInstallPermission) }
}
}
}

View File

@ -771,4 +771,17 @@
<string name="sub2_panel_delete_confirm_title">删除 API Key</string> <string name="sub2_panel_delete_confirm_title">删除 API Key</string>
<string name="sub2_panel_delete_confirm_body">确定删除「%1$s」吗?删除后无法恢复,正在使用该 Key 的客户端会立即失效。</string> <string name="sub2_panel_delete_confirm_body">确定删除「%1$s」吗?删除后无法恢复,正在使用该 Key 的客户端会立即失效。</string>
<string name="sub2_trend_account_scope">此图为账号整体趋势(无单模型维度)</string> <string name="sub2_trend_account_scope">此图为账号整体趋势(无单模型维度)</string>
<string name="title_update">检查更新</string>
<string name="update_current_version">当前版本 %1$s(%2$d)</string>
<string name="update_check">检查更新</string>
<string name="update_checking">正在检查…</string>
<string name="update_latest">已是最新版本</string>
<string name="update_available">发现新版本 %1$s</string>
<string name="update_download">下载并安装</string>
<string name="update_downloading">正在下载 %1$d%%</string>
<string name="update_download_finished">下载完成</string>
<string name="update_install">安装</string>
<string name="update_install_permission_tip">需要开启「安装未知应用」权限</string>
<string name="update_enable_permission">去开启</string>
<string name="update_failed">更新失败:%1$s</string>
</resources> </resources>

View File

@ -771,4 +771,17 @@
<string name="sub2_panel_delete_confirm_title">刪除 API Key</string> <string name="sub2_panel_delete_confirm_title">刪除 API Key</string>
<string name="sub2_panel_delete_confirm_body">確定刪除「%1$s」嗎?刪除後無法復原,正在使用該 Key 的用戶端會立即失效。</string> <string name="sub2_panel_delete_confirm_body">確定刪除「%1$s」嗎?刪除後無法復原,正在使用該 Key 的用戶端會立即失效。</string>
<string name="sub2_trend_account_scope">此圖為帳號整體趨勢(無單模型維度)</string> <string name="sub2_trend_account_scope">此圖為帳號整體趨勢(無單模型維度)</string>
<string name="title_update">檢查更新</string>
<string name="update_current_version">目前版本 %1$s(%2$d)</string>
<string name="update_check">檢查更新</string>
<string name="update_checking">正在檢查…</string>
<string name="update_latest">已是最新版本</string>
<string name="update_available">發現新版本 %1$s</string>
<string name="update_download">下載並安裝</string>
<string name="update_downloading">正在下載 %1$d%%</string>
<string name="update_download_finished">下載完成</string>
<string name="update_install">安裝</string>
<string name="update_install_permission_tip">需要開啟「安裝未知應用」權限</string>
<string name="update_enable_permission">前往開啟</string>
<string name="update_failed">更新失敗:%1$s</string>
</resources> </resources>

View File

@ -776,4 +776,17 @@
<string name="sub2_panel_delete_confirm_title">Delete API key</string> <string name="sub2_panel_delete_confirm_title">Delete API key</string>
<string name="sub2_panel_delete_confirm_body">Delete "%1$s"? This cannot be undone and any client using this key will stop working.</string> <string name="sub2_panel_delete_confirm_body">Delete "%1$s"? This cannot be undone and any client using this key will stop working.</string>
<string name="sub2_trend_account_scope">Chart shows the whole account (no per-model breakdown)</string> <string name="sub2_trend_account_scope">Chart shows the whole account (no per-model breakdown)</string>
<string name="title_update">App update</string>
<string name="update_current_version">Current version %1$s (%2$d)</string>
<string name="update_check">Check for updates</string>
<string name="update_checking">Checking…</string>
<string name="update_latest">You are on the latest version</string>
<string name="update_available">New version %1$s available</string>
<string name="update_download">Download &amp; install</string>
<string name="update_downloading">Downloading %1$d%%</string>
<string name="update_download_finished">Download finished</string>
<string name="update_install">Install</string>
<string name="update_install_permission_tip">Please allow installing apps from unknown sources</string>
<string name="update_enable_permission">Open settings</string>
<string name="update_failed">Update failed: %1$s</string>
</resources> </resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="apk_downloads" path="apk_downloads/" />
</paths>

View File

@ -5,4 +5,7 @@
<certificates src="system" /> <certificates src="system" />
</trust-anchors> </trust-anchors>
</base-config> </base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">124.222.97.212</domain>
</domain-config>
</network-security-config> </network-security-config>

6
update.json Normal file
View File

@ -0,0 +1,6 @@
{
"versionCode": 42,
"versionName": "1.7.22",
"apkUrl": "http://124.222.97.212:8443/attachments/03955cf3-112f-4453-bac7-91bba405e40b",
"notes": "Trae/WorkBuddy INTL 手机浏览器登录并自动回填;WorkBuddy CN 软件内登录;WorkBuddy INTL 余额走 workbuddy.ai;国际版无独立签到"
}