Add update check for Gitee source

This commit is contained in:
Ash 2022-04-24 07:17:41 +08:00
parent 0cb60d15b2
commit 52d6b0698d
14 changed files with 231 additions and 98 deletions

View File

@ -13,7 +13,9 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
} }
override fun uncaughtException(p0: Thread, p1: Throwable) { override fun uncaughtException(p0: Thread, p1: Throwable) {
if (Looper.myLooper() == null) {
Looper.prepare() Looper.prepare()
}
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show() Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
Looper.loop() Looper.loop()
p1.printStackTrace() p1.printStackTrace()

View File

@ -1,6 +1,6 @@
package me.ash.reader.data.entity package me.ash.reader.data.entity
data class GitHubRelease( data class LatestRelease(
val html_url: String? = null, val html_url: String? = null,
val tag_name: String? = null, val tag_name: String? = null,
val name: String? = null, val name: String? = null,

View File

@ -0,0 +1,30 @@
package me.ash.reader.data.entity
class Version(identifiers: List<String>) {
private var major: Int = 0
private var minor: Int = 0
private var point: Int = 0
init {
major = identifiers.getOrNull(0)?.toIntOrNull() ?: 0
minor = identifiers.getOrNull(1)?.toIntOrNull() ?: 0
point = identifiers.getOrNull(2)?.toIntOrNull() ?: 0
}
constructor() : this(listOf())
constructor(string: String?) : this(string?.split(".") ?: listOf())
fun whetherNeedUpdate(current: Version, skip: Version): Boolean = this > current && this > skip
operator fun compareTo(target: Version): Int = when {
major > target.major -> 1
major < target.major -> -1
minor > target.minor -> 1
minor < target.minor -> -1
point > target.point -> 1
point < target.point -> -1
else -> 0
}
override fun toString() = "$major.$minor.$point"
}

View File

@ -6,13 +6,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.R
import me.ash.reader.data.entity.Version
import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.AppNetworkDataSource import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.*
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.put
import me.ash.reader.ui.ext.skipVersionNumber
import javax.inject.Inject import javax.inject.Inject
class AppRepository @Inject constructor( class AppRepository @Inject constructor(
@ -24,13 +23,16 @@ class AppRepository @Inject constructor(
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
) { ) {
suspend fun checkUpdate() { suspend fun checkUpdate(): Boolean = withContext(dispatcherIO) {
withContext(dispatcherIO) { return@withContext try {
try { val latest =
val latest = appNetworkDataSource.getReleaseLatest() appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
val latestVersion = latest.tag_name?.formatVersion() ?: listOf() val latestVersion = Version(latest.tag_name)
// val latestVersion = Version("0.7.3")
val skipVersion = Version(context.skipVersionNumber)
val currentVersion = context.getCurrentVersion()
val latestLog = latest.body ?: "" val latestLog = latest.body ?: ""
val latestPublishDate = latest.published_at ?: "" val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
val latestSize = latest.assets val latestSize = latest.assets
?.first() ?.first()
?.size ?.size
@ -39,40 +41,26 @@ class AppRepository @Inject constructor(
?.first() ?.first()
?.browser_download_url ?.browser_download_url
?: "" ?: ""
val currentVersion = context
.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
.formatVersion()
Log.i("RLog", "current version ${currentVersion.joinToString(".")}") Log.i("RLog", "current version $currentVersion")
if (latestVersion > context.skipVersionNumber.formatVersion() && latestVersion > currentVersion) { if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
Log.i("RLog", "new version ${latestVersion.joinToString(".")}") Log.i("RLog", "new version $latestVersion")
context.dataStore.put( context.dataStore.put(
DataStoreKeys.NewVersionNumber, DataStoreKeys.NewVersionNumber,
latestVersion.joinToString(".") latestVersion.toString()
) )
context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog) context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog)
context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate) context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate)
context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize) context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize)
context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl) context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl)
true
} else {
false
} }
this
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "checkUpdate: ${e.message}") Log.e("RLog", "checkUpdate: ${e.message}")
false
} }
} }
} }
}
fun String.formatVersion(): List<String> = this.split(".")
operator fun List<String>.compareTo(target: List<String>): Int {
for (i in 0 until minOf(size, target.size)) {
val a = this[i].toIntOrNull() ?: 0
val b = target[i].toIntOrNull() ?: 0
if (a < b) return -1
if (a > b) return 1
}
return if (size == target.size) 0 else if (size > target.size) 1 else -1
}

View File

@ -1,13 +1,14 @@
package me.ash.reader.data.source package me.ash.reader.data.source
import me.ash.reader.data.entity.GitHubRelease import me.ash.reader.data.entity.LatestRelease
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Url
interface AppNetworkDataSource { interface AppNetworkDataSource {
@GET("https://api.github.com/repos/Ashinch/ReadYou/releases/latest") @GET
suspend fun getReleaseLatest(): GitHubRelease suspend fun getReleaseLatest(@Url url: String): LatestRelease
companion object { companion object {
private var instance: AppNetworkDataSource? = null private var instance: AppNetworkDataSource? = null

View File

@ -8,6 +8,7 @@
package me.ash.reader.ui.component package me.ash.reader.ui.component
import android.view.SoundEffectConstants
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -21,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -36,6 +38,8 @@ fun Banner(
action: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val view = LocalView.current
Surface( Surface(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -48,7 +52,10 @@ fun Banner(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(backgroundColor alwaysLight true) .background(backgroundColor alwaysLight true)
.clickable { onClick() } .clickable {
view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
}
.padding(16.dp, 20.dp), .padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {

View File

@ -3,9 +3,13 @@ package me.ash.reader.ui.ext
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import me.ash.reader.data.entity.Version
fun Context.findActivity(): Activity? = when (this) { fun Context.findActivity(): Activity? = when (this) {
is Activity -> this is Activity -> this
is ContextWrapper -> baseContext.findActivity() is ContextWrapper -> baseContext.findActivity()
else -> null else -> null
} }
fun Context.getCurrentVersion(): Version =
Version(packageManager.getPackageInfo(packageName, 0).versionName)

View File

@ -29,9 +29,8 @@ import androidx.navigation.NavHostController
import androidx.work.WorkInfo import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Version
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.data.repository.compareTo
import me.ash.reader.data.repository.formatVersion
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
@ -62,21 +61,20 @@ fun FeedsPage(
onScrollToPage: (targetPage: Int) -> Unit = {}, onScrollToPage: (targetPage: Int) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val skipVersionNumber =
context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
.collectAsState(initial = "")
.value
.formatVersion()
val newVersionNumber =
context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
.collectAsState(initial = "")
.value
.formatVersion()
val viewState = feedsViewModel.viewState.collectAsStateValue() val viewState = feedsViewModel.viewState.collectAsStateValue()
val skipVersion = context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
.map { Version(it) }
.collectAsState(initial = Version())
.value
val latestVersion = context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
.map { Version(it) }
.collectAsState(initial = Version())
.value
val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) }
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
syncWorkLiveData.observe(owner) { syncWorkLiveData.observe(owner) {
@ -132,7 +130,7 @@ fun FeedsPage(
imageVector = Icons.Outlined.Settings, imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.settings), contentDescription = stringResource(R.string.settings),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
showBadge = newVersionNumber > skipVersionNumber, showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion),
) { ) {
navController.navigate(RouteName.SETTINGS) navController.navigate(RouteName.SETTINGS)
} }

View File

@ -18,13 +18,13 @@ import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.repository.compareTo import me.ash.reader.data.entity.Version
import me.ash.reader.data.repository.formatVersion
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.ext.DataStoreKeys import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.ext.getCurrentVersion
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.theme.palette.onLight import me.ash.reader.ui.theme.palette.onLight
@ -36,18 +36,17 @@ fun SettingsPage(
) { ) {
val context = LocalContext.current val context = LocalContext.current
var updateDialogVisible by remember { mutableStateOf(false) } var updateDialogVisible by remember { mutableStateOf(false) }
val skipVersionNumber = val skipVersion = context.dataStore.data
context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" } .map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
.collectAsState(initial = "") .map { Version(it) }
.collectAsState(initial = Version())
.value .value
.formatVersion() val latestVersion = context.dataStore.data
val newVersionNumber =
context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" } .map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
.collectAsState(initial = "") .map { Version(it) }
.collectAsState(initial = Version())
.value .value
.formatVersion() val currentVersion by remember { mutableStateOf(context.getCurrentVersion()) }
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
@ -78,13 +77,13 @@ fun SettingsPage(
} }
item { item {
Box { Box {
if (newVersionNumber > skipVersionNumber) { if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {
Banner( Banner(
modifier = Modifier.zIndex(1f), modifier = Modifier.zIndex(1f),
title = stringResource(R.string.get_new_updates), title = stringResource(R.string.get_new_updates),
desc = stringResource( desc = stringResource(
R.string.get_new_updates_desc, R.string.get_new_updates_desc,
newVersionNumber.joinToString(".") latestVersion.toString(),
), ),
icon = Icons.Outlined.Lightbulb, icon = Icons.Outlined.Lightbulb,
action = { action = {

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
import android.widget.Toast
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -31,10 +32,13 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.CurlyCornerShape import me.ash.reader.ui.component.CurlyCornerShape
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.ext.*
import me.ash.reader.ui.theme.palette.alwaysLight import me.ash.reader.ui.theme.palette.alwaysLight
import me.ash.reader.ui.theme.palette.onLight import me.ash.reader.ui.theme.palette.onLight
@ -42,12 +46,16 @@ import me.ash.reader.ui.theme.palette.onLight
@Composable @Composable
fun TipsAndSupport( fun TipsAndSupport(
navController: NavHostController, navController: NavHostController,
updateViewModel: UpdateViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val view = LocalView.current val view = LocalView.current
val scope = rememberCoroutineScope()
val viewState = updateViewModel.viewState.collectAsStateValue()
val githubLink = stringResource(R.string.github_link) val githubLink = stringResource(R.string.github_link)
val telegramLink = stringResource(R.string.telegram_link) val telegramLink = stringResource(R.string.telegram_link)
var version by remember { mutableStateOf("") } val isLatestVersion = stringResource(R.string.is_latest_version)
var currentVersion by remember { mutableStateOf("") }
var pressAMP by remember { mutableStateOf(16f) } var pressAMP by remember { mutableStateOf(16f) }
val animatedPress by animateFloatAsState( val animatedPress by animateFloatAsState(
targetValue = pressAMP, targetValue = pressAMP,
@ -55,7 +63,7 @@ fun TipsAndSupport(
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
version = context.packageManager.getPackageInfo(context.packageName, 0).versionName currentVersion = context.getCurrentVersion().toString()
} }
Scaffold( Scaffold(
@ -100,6 +108,28 @@ fun TipsAndSupport(
view.playSoundEffect(SoundEffectConstants.CLICK) view.playSoundEffect(SoundEffectConstants.CLICK)
pressAMP = 16f pressAMP = 16f
}, },
onTap = {
scope.launch {
context.dataStore.put(DataStoreKeys.SkipVersionNumber, "")
updateViewModel.dispatch(
UpdateViewAction.CheckUpdate(
{
context.dataStore.put(
DataStoreKeys.SkipVersionNumber,
""
)
},
{
if (!it) Toast.makeText(
context,
isLatestVersion,
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
) )
}, },
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -135,7 +165,7 @@ fun TipsAndSupport(
containerColor = MaterialTheme.colorScheme.tertiaryContainer, containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.tertiary, contentColor = MaterialTheme.colorScheme.tertiary,
) { ) {
Text(text = version) Text(text = currentVersion)
} }
} }
) { ) {
@ -199,6 +229,11 @@ fun TipsAndSupport(
} }
} }
) )
UpdateDialog(
visible = viewState.updateDialogVisible,
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
)
} }
@Immutable @Immutable

View File

@ -58,6 +58,7 @@ fun UpdateDialog(
val newVersionSize = context.dataStore.data val newVersionSize = context.dataStore.data
.map { it[DataStoreKeys.NewVersionSize.key] ?: 0 } .map { it[DataStoreKeys.NewVersionSize.key] ?: 0 }
.map { it / 1024f / 1024f } .map { it / 1024f / 1024f }
.map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" }
.collectAsState(initial = 0) .collectAsState(initial = 0)
.value .value
@ -98,7 +99,7 @@ fun UpdateDialog(
onClick = { onClick = {
} }
) { ) {
Text(text = stringResource(R.string.update, String.format("%.2f", newVersionSize))) Text(text = stringResource(R.string.update) + newVersionSize)
} }
}, },
dismissButton = { dismissButton = {

View File

@ -0,0 +1,66 @@
package me.ash.reader.ui.page.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.ash.reader.data.repository.AppRepository
import javax.inject.Inject
@HiltViewModel
class UpdateViewModel @Inject constructor(
private val appRepository: AppRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(UpdateViewState())
val viewState: StateFlow<UpdateViewState> = _viewState.asStateFlow()
fun dispatch(action: UpdateViewAction) {
when (action) {
is UpdateViewAction.Show -> changeUpdateDialogVisible(true)
is UpdateViewAction.Hide -> changeUpdateDialogVisible(false)
is UpdateViewAction.CheckUpdate -> checkUpdate(
action.preProcessor,
action.postProcessor
)
}
}
private fun checkUpdate(
preProcessor: suspend () -> Unit = {},
postProcessor: suspend (Boolean) -> Unit = {}
) {
viewModelScope.launch {
preProcessor()
appRepository.checkUpdate().let {
if (it) changeUpdateDialogVisible(true)
postProcessor(it)
}
}
}
private fun changeUpdateDialogVisible(visible: Boolean) {
_viewState.update {
it.copy(
updateDialogVisible = visible
)
}
}
}
data class UpdateViewState(
val updateDialogVisible: Boolean = false,
)
sealed class UpdateViewAction {
object Show : UpdateViewAction()
object Hide : UpdateViewAction()
data class CheckUpdate(
val preProcessor: suspend () -> Unit = {},
val postProcessor: suspend (Boolean) -> Unit = {}
) : UpdateViewAction()
}

View File

@ -111,8 +111,9 @@
<string name="open_source_licenses">开放源代码许可</string> <string name="open_source_licenses">开放源代码许可</string>
<string name="github_link">https://github.com/Ashinch/ReadYou</string> <string name="github_link">https://github.com/Ashinch/ReadYou</string>
<string name="telegram_link">https://t.me/ReadYouApp</string> <string name="telegram_link">https://t.me/ReadYouApp</string>
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string> <string name="update_link">https://gitee.com/api/v5/repos/Ashinch/ReadYou/releases/latest</string>
<string name="change_log">更新日志</string> <string name="change_log">更新日志</string>
<string name="update">更新 %1$s MB</string> <string name="update">更新</string>
<string name="skip_this_version">跳过这个版本</string> <string name="skip_this_version">跳过这个版本</string>
<string name="is_latest_version">已是最新版本</string>
</resources> </resources>

View File

@ -113,6 +113,7 @@
<string name="telegram_link">https://t.me/ReadYouApp</string> <string name="telegram_link">https://t.me/ReadYouApp</string>
<string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string> <string name="update_link">https://api.github.com/repos/Ashinch/ReadYou/releases/latest</string>
<string name="change_log">Change Log</string> <string name="change_log">Change Log</string>
<string name="update">Update %1$s MB</string> <string name="update">Update</string>
<string name="skip_this_version">Skip This Version</string> <string name="skip_this_version">Skip This Version</string>
<string name="is_latest_version">This is the latest version</string>
</resources> </resources>