Compare commits
No commits in common. "dev" and "debug-build" have entirely different histories.
dev
...
debug-buil
@ -76,16 +76,6 @@
|
|||||||
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>
|
||||||
@ -40,9 +40,7 @@ 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
|
||||||
}
|
}
|
||||||
@ -239,9 +237,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() }
|
||||||
// 只允许读取与快照同一账号的暂存凭据;null=默认账号也要看快照当初请求的是不是默认账号,
|
requested == null ||
|
||||||
// 避免 A 账号刷新期间把 A 的暂存凭据误交给默认账号(B)的读请求。
|
session.snapshot.accountId == null ||
|
||||||
requested == session.snapshot.requestedAccountId
|
session.snapshot.accountId == requested
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -370,8 +368,7 @@ 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() }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,7 +19,6 @@ 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
|
||||||
@ -205,16 +204,6 @@ 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(
|
||||||
|
|||||||
@ -102,8 +102,6 @@ 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
|
||||||
@ -349,45 +347,28 @@ 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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
shape = RoundedCornerShape(16.dp),
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
text = "💡 " + stringResource(tipTextRes),
|
||||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
) {
|
color = inkMuted(),
|
||||||
Text(
|
modifier = Modifier.weight(1f)
|
||||||
text = "💡 " + stringResource(tipTextRes),
|
)
|
||||||
style = MaterialTheme.typography.bodySmall,
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
color = inkMuted(),
|
Text(
|
||||||
modifier = Modifier.weight(1f)
|
text = stringResource(R.string.auto_checkin_switch_label),
|
||||||
)
|
style = MaterialTheme.typography.labelSmall,
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
color = inkMuted()
|
||||||
Text(
|
)
|
||||||
text = stringResource(R.string.auto_checkin_switch_label),
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
style = MaterialTheme.typography.labelMedium,
|
Switch(
|
||||||
color = inkWarm(),
|
checked = autoCheckinEnabled,
|
||||||
fontWeight = FontWeight.SemiBold
|
onCheckedChange = viewModel::setAutoCheckinEnabled,
|
||||||
)
|
colors = SwitchDefaults.colors(checkedThumbColor = StrawberryPink)
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
)
|
||||||
Switch(
|
|
||||||
checked = autoCheckinEnabled,
|
|
||||||
onCheckedChange = viewModel::setAutoCheckinEnabled,
|
|
||||||
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,
|
||||||
@ -941,8 +922,6 @@ 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))
|
||||||
|
|||||||
@ -296,8 +296,6 @@ 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(
|
||||||
|
|||||||
@ -54,11 +54,6 @@ 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
|
||||||
@ -165,9 +160,6 @@ 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()
|
||||||
@ -318,142 +310,6 @@ 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(
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -771,17 +771,4 @@
|
|||||||
<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>
|
||||||
@ -771,17 +771,4 @@
|
|||||||
<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>
|
||||||
@ -776,17 +776,4 @@
|
|||||||
<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 & 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>
|
||||||
@ -1,4 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -5,7 +5,4 @@
|
|||||||
<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>
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"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;国际版无独立签到"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user