diff --git a/app/src/main/java/me/ash/reader/CrashHandler.kt b/app/src/main/java/me/ash/reader/CrashHandler.kt index 12cbfbb..fce71a2 100644 --- a/app/src/main/java/me/ash/reader/CrashHandler.kt +++ b/app/src/main/java/me/ash/reader/CrashHandler.kt @@ -13,11 +13,13 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler { } override fun uncaughtException(p0: Thread, p1: Throwable) { - Looper.prepare() + if (Looper.myLooper() == null) { + Looper.prepare() + } Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show() Looper.loop() p1.printStackTrace() - Log.e("RLog", "uncaughtException: ${p1.message}" ) + Log.e("RLog", "uncaughtException: ${p1.message}") android.os.Process.killProcess(android.os.Process.myPid()); exitProcess(1) } diff --git a/app/src/main/java/me/ash/reader/data/entity/GitHubRelease.kt b/app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt similarity index 95% rename from app/src/main/java/me/ash/reader/data/entity/GitHubRelease.kt rename to app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt index ab65a43..000e7c9 100644 --- a/app/src/main/java/me/ash/reader/data/entity/GitHubRelease.kt +++ b/app/src/main/java/me/ash/reader/data/entity/LatestRelease.kt @@ -1,6 +1,6 @@ package me.ash.reader.data.entity -data class GitHubRelease( +data class LatestRelease( val html_url: String? = null, val tag_name: String? = null, val name: String? = null, diff --git a/app/src/main/java/me/ash/reader/data/entity/Version.kt b/app/src/main/java/me/ash/reader/data/entity/Version.kt new file mode 100644 index 0000000..f362526 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/entity/Version.kt @@ -0,0 +1,30 @@ +package me.ash.reader.data.entity + +class Version(identifiers: List) { + 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" +} diff --git a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt index c692c3c..4948c6e 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt @@ -6,13 +6,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope 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.DispatcherIO import me.ash.reader.data.source.AppNetworkDataSource -import me.ash.reader.ui.ext.DataStoreKeys -import me.ash.reader.ui.ext.dataStore -import me.ash.reader.ui.ext.put -import me.ash.reader.ui.ext.skipVersionNumber +import me.ash.reader.ui.ext.* import javax.inject.Inject class AppRepository @Inject constructor( @@ -24,55 +23,44 @@ class AppRepository @Inject constructor( @DispatcherIO private val dispatcherIO: CoroutineDispatcher, ) { - suspend fun checkUpdate() { - withContext(dispatcherIO) { - try { - val latest = appNetworkDataSource.getReleaseLatest() - val latestVersion = latest.tag_name?.formatVersion() ?: listOf() - val latestLog = latest.body ?: "" - val latestPublishDate = latest.published_at ?: "" - val latestSize = latest.assets - ?.first() - ?.size - ?: 0 - val latestDownloadUrl = latest.assets - ?.first() - ?.browser_download_url - ?: "" - val currentVersion = context - .packageManager - .getPackageInfo(context.packageName, 0) - .versionName - .formatVersion() + suspend fun checkUpdate(): Boolean = withContext(dispatcherIO) { + return@withContext try { + val latest = + appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link)) + 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 latestPublishDate = latest.published_at ?: latest.created_at ?: "" + val latestSize = latest.assets + ?.first() + ?.size + ?: 0 + val latestDownloadUrl = latest.assets + ?.first() + ?.browser_download_url + ?: "" - Log.i("RLog", "current version ${currentVersion.joinToString(".")}") - if (latestVersion > context.skipVersionNumber.formatVersion() && latestVersion > currentVersion) { - Log.i("RLog", "new version ${latestVersion.joinToString(".")}") - context.dataStore.put( - DataStoreKeys.NewVersionNumber, - latestVersion.joinToString(".") - ) - context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog) - context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate) - context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize) - context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl) - } - this - } catch (e: Exception) { - Log.e("RLog", "checkUpdate: ${e.message}") + Log.i("RLog", "current version $currentVersion") + if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { + Log.i("RLog", "new version $latestVersion") + context.dataStore.put( + DataStoreKeys.NewVersionNumber, + latestVersion.toString() + ) + context.dataStore.put(DataStoreKeys.NewVersionLog, latestLog) + context.dataStore.put(DataStoreKeys.NewVersionPublishDate, latestPublishDate) + context.dataStore.put(DataStoreKeys.NewVersionSize, latestSize) + context.dataStore.put(DataStoreKeys.NewVersionDownloadUrl, latestDownloadUrl) + true + } else { + false } + } catch (e: Exception) { + e.printStackTrace() + Log.e("RLog", "checkUpdate: ${e.message}") + false } } -} - -fun String.formatVersion(): List = this.split(".") - -operator fun List.compareTo(target: List): 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 } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt b/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt index 9e7e1d6..1935615 100644 --- a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt @@ -1,13 +1,14 @@ 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.converter.gson.GsonConverterFactory import retrofit2.http.GET +import retrofit2.http.Url interface AppNetworkDataSource { - @GET("https://api.github.com/repos/Ashinch/ReadYou/releases/latest") - suspend fun getReleaseLatest(): GitHubRelease + @GET + suspend fun getReleaseLatest(@Url url: String): LatestRelease companion object { private var instance: AppNetworkDataSource? = null diff --git a/app/src/main/java/me/ash/reader/ui/component/Banner.kt b/app/src/main/java/me/ash/reader/ui/component/Banner.kt index ef12aff..aa3f0f8 100644 --- a/app/src/main/java/me/ash/reader/ui/component/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/component/Banner.kt @@ -8,6 +8,7 @@ package me.ash.reader.ui.component +import android.view.SoundEffectConstants import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -36,6 +38,8 @@ fun Banner( action: (@Composable () -> Unit)? = null, onClick: () -> Unit = {}, ) { + val view = LocalView.current + Surface( modifier = modifier .fillMaxWidth() @@ -48,7 +52,10 @@ fun Banner( .padding(horizontal = 16.dp) .clip(RoundedCornerShape(32.dp)) .background(backgroundColor alwaysLight true) - .clickable { onClick() } + .clickable { + view.playSoundEffect(SoundEffectConstants.CLICK) + onClick() + } .padding(16.dp, 20.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt index b7f864b..d2f6a43 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt @@ -3,9 +3,13 @@ package me.ash.reader.ui.ext import android.app.Activity import android.content.Context import android.content.ContextWrapper +import me.ash.reader.data.entity.Version fun Context.findActivity(): Activity? = when (this) { is Activity -> this is ContextWrapper -> baseContext.findActivity() else -> null -} \ No newline at end of file +} + +fun Context.getCurrentVersion(): Version = + Version(packageManager.getPackageInfo(packageName, 0).versionName) \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 6bf0243..f012f9b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -29,9 +29,8 @@ import androidx.navigation.NavHostController import androidx.work.WorkInfo import kotlinx.coroutines.flow.map 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.compareTo -import me.ash.reader.data.repository.formatVersion import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.FeedbackIconButton @@ -62,21 +61,20 @@ fun FeedsPage( onScrollToPage: (targetPage: Int) -> Unit = {}, ) { 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 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 var isSyncing by remember { mutableStateOf(false) } syncWorkLiveData.observe(owner) { @@ -132,7 +130,7 @@ fun FeedsPage( imageVector = Icons.Outlined.Settings, contentDescription = stringResource(R.string.settings), tint = MaterialTheme.colorScheme.onSurface, - showBadge = newVersionNumber > skipVersionNumber, + showBadge = latestVersion.whetherNeedUpdate(currentVersion, skipVersion), ) { navController.navigate(RouteName.SETTINGS) } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt index 85754f2..e7589fd 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt @@ -18,13 +18,13 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavHostController import kotlinx.coroutines.flow.map import me.ash.reader.R -import me.ash.reader.data.repository.compareTo -import me.ash.reader.data.repository.formatVersion +import me.ash.reader.data.entity.Version import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.ext.DataStoreKeys 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.theme.palette.onLight @@ -36,18 +36,17 @@ fun SettingsPage( ) { val context = LocalContext.current var updateDialogVisible by remember { mutableStateOf(false) } - 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 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()) } Scaffold( modifier = Modifier @@ -78,13 +77,13 @@ fun SettingsPage( } item { Box { - if (newVersionNumber > skipVersionNumber) { + if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { Banner( modifier = Modifier.zIndex(1f), title = stringResource(R.string.get_new_updates), desc = stringResource( R.string.get_new_updates_desc, - newVersionNumber.joinToString(".") + latestVersion.toString(), ), icon = Icons.Outlined.Lightbulb, action = { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt b/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt index d9da011..bae04a1 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.net.Uri import android.view.HapticFeedbackConstants import android.view.SoundEffectConstants +import android.widget.Toast import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState 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.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController +import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.ui.component.CurlyCornerShape 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.onLight @@ -42,12 +46,16 @@ import me.ash.reader.ui.theme.palette.onLight @Composable fun TipsAndSupport( navController: NavHostController, + updateViewModel: UpdateViewModel = hiltViewModel(), ) { val context = LocalContext.current val view = LocalView.current + val scope = rememberCoroutineScope() + val viewState = updateViewModel.viewState.collectAsStateValue() val githubLink = stringResource(R.string.github_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) } val animatedPress by animateFloatAsState( targetValue = pressAMP, @@ -55,7 +63,7 @@ fun TipsAndSupport( ) LaunchedEffect(Unit) { - version = context.packageManager.getPackageInfo(context.packageName, 0).versionName + currentVersion = context.getCurrentVersion().toString() } Scaffold( @@ -100,6 +108,28 @@ fun TipsAndSupport( view.playSoundEffect(SoundEffectConstants.CLICK) 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, @@ -135,7 +165,7 @@ fun TipsAndSupport( containerColor = MaterialTheme.colorScheme.tertiaryContainer, 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 diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt index d737c38..d9d0529 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt @@ -58,6 +58,7 @@ fun UpdateDialog( val newVersionSize = context.dataStore.data .map { it[DataStoreKeys.NewVersionSize.key] ?: 0 } .map { it / 1024f / 1024f } + .map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" } .collectAsState(initial = 0) .value @@ -98,7 +99,7 @@ fun UpdateDialog( onClick = { } ) { - Text(text = stringResource(R.string.update, String.format("%.2f", newVersionSize))) + Text(text = stringResource(R.string.update) + newVersionSize) } }, dismissButton = { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt new file mode 100644 index 0000000..fdf3570 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt @@ -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 = _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() +} \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 83ac161..e758b2d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -111,8 +111,9 @@ 开放源代码许可 https://github.com/Ashinch/ReadYou https://t.me/ReadYouApp - https://api.github.com/repos/Ashinch/ReadYou/releases/latest + https://gitee.com/api/v5/repos/Ashinch/ReadYou/releases/latest 更新日志 - 更新 %1$s MB + 更新 跳过这个版本 + 已是最新版本 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a88331a..c0c34c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ https://t.me/ReadYouApp https://api.github.com/repos/Ashinch/ReadYou/releases/latest Change Log - Update %1$s MB + Update Skip This Version + This is the latest version \ No newline at end of file