1.7 检查更新

This commit is contained in:
2026-11-04 17:14:38 +08:00
parent 13e296f106
commit 85aaa4f285
5 changed files with 154 additions and 17 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ android {
applicationId = "net.bluemangoo.indpjsk"
minSdk = appMinSdk
targetSdk = 37
versionCode = 7
versionName = "1.6"
versionCode = 8
versionName = "1.7"
}
@@ -0,0 +1,88 @@
package net.bluemangoo.indpjsk.data
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import net.bluemangoo.indpjsk.BuildConfig
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean
data class UpdateInfo(
val latestVersion: String,
val downloadUrl: String,
)
object UpdateChecker {
const val DOWNLOAD_URL = "https://indpjsk.bluemangoo.net/download?source=app_update"
private const val LATEST_URL = "https://indpjsk.bluemangoo.net/latest"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val started = AtomicBoolean(false)
private val _update = MutableStateFlow<UpdateInfo?>(null)
val update: StateFlow<UpdateInfo?> = _update.asStateFlow()
fun ensureChecked() {
if (!started.compareAndSet(false, true)) return
scope.launch {
runCatching { fetchLatestVersion() }
.onSuccess { latest ->
if (latest != null && isNewer(latest, BuildConfig.VERSION_NAME)) {
_update.value = UpdateInfo(latest, DOWNLOAD_URL)
}
}
.onFailure {
started.set(false)
}
}
}
private fun fetchLatestVersion(): String? {
val conn = URL(LATEST_URL).openConnection() as HttpURLConnection
try {
conn.instanceFollowRedirects = true
conn.connectTimeout = 10_000
conn.readTimeout = 15_000
if (conn.responseCode != 200) return null
val body = conn.inputStream.bufferedReader().use { it.readText() }.trim()
if (body.isEmpty()) return null
if (body.startsWith("{")) {
return JSONObject(body).optString("tag").trim().takeIf { it.isNotEmpty() }
}
return body
} finally {
conn.disconnect()
}
}
internal fun isNewer(remote: String, current: String): Boolean {
fun parts(v: String): List<String> = v.trim()
.removePrefix("v")
.removePrefix("V")
.split('.', '-', '+', '_')
.filter { it.isNotEmpty() }
val r = parts(remote)
val c = parts(current)
for (i in 0 until maxOf(r.size, c.size)) {
val rs = r.getOrNull(i) ?: ""
val cs = c.getOrNull(i) ?: ""
val rn = rs.toIntOrNull()
val cn = cs.toIntOrNull()
if (rn != null && cn != null) {
if (rn != cn) return rn > cn
} else {
val cmp = rs.compareTo(cs, ignoreCase = true)
if (cmp != 0) return cmp > 0
}
}
return false
}
}
@@ -1,26 +1,18 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
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.runtime.Composable
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.dp
import net.bluemangoo.indpjsk.BuildConfig
import net.bluemangoo.indpjsk.R
import net.bluemangoo.indpjsk.data.UpdateChecker
import net.bluemangoo.indpjsk.ui.theme.indPjskCustomColor
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -28,6 +20,9 @@ fun AboutScreen(
readme: String,
onBack: () -> Unit,
) {
val update by UpdateChecker.update.collectAsState()
LaunchedEffect(Unit) { UpdateChecker.ensureChecked() }
Scaffold(
topBar = {
TopAppBar(
@@ -54,6 +49,10 @@ fun AboutScreen(
text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
style = MaterialTheme.typography.titleMedium
)
update?.let { info ->
Spacer(Modifier.height(4.dp))
UpdateLine(info.latestVersion)
}
Spacer(Modifier.height(8.dp))
HorizontalDivider()
Spacer(Modifier.height(8.dp))
@@ -65,3 +64,30 @@ fun AboutScreen(
}
}
}
@Composable
private fun UpdateLine(version: String) {
val warning = indPjskCustomColor().warning
val text = remember(version) {
buildAnnotatedString {
withStyle(SpanStyle(color = warning)) {
append("发现新的版本 $version,")
}
withLink(
LinkAnnotation.Url(
url = UpdateChecker.DOWNLOAD_URL,
)
) {
append("点击")
}
withStyle(SpanStyle(color = warning)) {
append("下载")
}
}
}
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
softWrap = true
)
}
@@ -8,4 +8,7 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Pink40 = Color(0xFF7D5260)
val Warning = Color(0xFFD97706)
val WarningDark = Color(0xFFFBBF24)
@@ -4,8 +4,28 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
data class CustomColorScheme(
val warning: Color,
)
private val customLightColorScheme = CustomColorScheme(
warning = Warning,
)
private val customDarkColorScheme = CustomColorScheme(
warning = WarningDark,
)
@Composable
fun indPjskCustomColor(
darkTheme: Boolean = isSystemInDarkTheme(),
): CustomColorScheme {
return if (darkTheme) customDarkColorScheme else customLightColorScheme
}
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,