Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a7e507d4
|
||
|
|
b23514a77a
|
||
|
|
85aaa4f285
|
+12
-3
@@ -18,17 +18,26 @@ android {
|
||||
applicationId = "net.bluemangoo.indpjsk"
|
||||
minSdk = appMinSdk
|
||||
targetSdk = 37
|
||||
versionCode = 7
|
||||
versionName = "1.6"
|
||||
versionCode = 8
|
||||
versionName = "1.7"
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
packaging {
|
||||
dex {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.edit
|
||||
import net.bluemangoo.indpjsk.App
|
||||
import net.bluemangoo.indpjsk.upload.UploadStore
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
@@ -24,7 +25,7 @@ object SettingsStore {
|
||||
prefs = { prefs() }
|
||||
)
|
||||
|
||||
var uploadProfilesJson by pref<String>()
|
||||
var uploadProfilesJson by pref<String>(default = UploadStore.encode(emptyList()))
|
||||
var upstreamProxy by pref<String>()
|
||||
var useProxy by pref<Boolean>()
|
||||
var bypassPackages by pref<Set<String>>()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user