1.7 检查更新
This commit is contained in:
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId = "net.bluemangoo.indpjsk"
|
applicationId = "net.bluemangoo.indpjsk"
|
||||||
minSdk = appMinSdk
|
minSdk = appMinSdk
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 7
|
versionCode = 8
|
||||||
versionName = "1.6"
|
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
|
package net.bluemangoo.indpjsk.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.*
|
||||||
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.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.runtime.*
|
||||||
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.bluemangoo.indpjsk.BuildConfig
|
import net.bluemangoo.indpjsk.BuildConfig
|
||||||
import net.bluemangoo.indpjsk.R
|
import net.bluemangoo.indpjsk.R
|
||||||
|
import net.bluemangoo.indpjsk.data.UpdateChecker
|
||||||
|
import net.bluemangoo.indpjsk.ui.theme.indPjskCustomColor
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -28,6 +20,9 @@ fun AboutScreen(
|
|||||||
readme: String,
|
readme: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val update by UpdateChecker.update.collectAsState()
|
||||||
|
LaunchedEffect(Unit) { UpdateChecker.ensureChecked() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -54,6 +49,10 @@ fun AboutScreen(
|
|||||||
text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
update?.let { info ->
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
UpdateLine(info.latestVersion)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Spacer(Modifier.height(8.dp))
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ val Pink80 = Color(0xFFEFB8C8)
|
|||||||
val Purple40 = Color(0xFF6650a4)
|
val Purple40 = Color(0xFF6650a4)
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
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.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
secondary = PurpleGrey80,
|
secondary = PurpleGrey80,
|
||||||
|
|||||||
Reference in New Issue
Block a user