Add update check for GitHub source
This commit is contained in:
parent
3c630e8a5b
commit
0cb60d15b2
|
@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
import me.ash.reader.data.module.DispatcherDefault
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
import me.ash.reader.data.repository.*
|
import me.ash.reader.data.repository.*
|
||||||
|
import me.ash.reader.data.source.AppNetworkDataSource
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
import me.ash.reader.data.source.ReaderDatabase
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
@ -30,6 +31,9 @@ class App : Application(), Configuration.Provider {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var workManager: WorkManager
|
lateinit var workManager: WorkManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appNetworkDataSource: AppNetworkDataSource
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
lateinit var opmlLocalDataSource: OpmlLocalDataSource
|
||||||
|
|
||||||
|
@ -39,6 +43,9 @@ class App : Application(), Configuration.Provider {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var rssHelper: RssHelper
|
lateinit var rssHelper: RssHelper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appRepository: AppRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var stringsRepository: StringsRepository
|
lateinit var stringsRepository: StringsRepository
|
||||||
|
|
||||||
|
@ -72,6 +79,7 @@ class App : Application(), Configuration.Provider {
|
||||||
applicationScope.launch(dispatcherDefault) {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
accountInit()
|
accountInit()
|
||||||
workerInit()
|
workerInit()
|
||||||
|
checkUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +98,10 @@ class App : Application(), Configuration.Provider {
|
||||||
rssRepository.get().doSync()
|
rssRepository.get().doSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun checkUpdate() {
|
||||||
|
appRepository.checkUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration =
|
override fun getWorkManagerConfiguration(): Configuration =
|
||||||
Configuration.Builder()
|
Configuration.Builder()
|
||||||
.setWorkerFactory(workerFactory)
|
.setWorkerFactory(workerFactory)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package me.ash.reader
|
package me.ash.reader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import java.lang.Thread.UncaughtExceptionHandler
|
import java.lang.Thread.UncaughtExceptionHandler
|
||||||
|
@ -12,7 +13,9 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
override fun uncaughtException(p0: Thread, p1: Throwable) {
|
||||||
|
Looper.prepare()
|
||||||
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
|
||||||
|
Looper.loop()
|
||||||
p1.printStackTrace()
|
p1.printStackTrace()
|
||||||
Log.e("RLog", "uncaughtException: ${p1.message}" )
|
Log.e("RLog", "uncaughtException: ${p1.message}" )
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
|
23
app/src/main/java/me/ash/reader/data/entity/GitHubRelease.kt
Normal file
23
app/src/main/java/me/ash/reader/data/entity/GitHubRelease.kt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package me.ash.reader.data.entity
|
||||||
|
|
||||||
|
data class GitHubRelease(
|
||||||
|
val html_url: String? = null,
|
||||||
|
val tag_name: String? = null,
|
||||||
|
val name: String? = null,
|
||||||
|
val draft: Boolean? = null,
|
||||||
|
val prerelease: Boolean? = null,
|
||||||
|
val created_at: String? = null,
|
||||||
|
val published_at: String? = null,
|
||||||
|
val assets: List<AssetsItem>? = null,
|
||||||
|
val body: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AssetsItem(
|
||||||
|
val name: String? = null,
|
||||||
|
val content_type: String? = null,
|
||||||
|
val size: Int? = null,
|
||||||
|
val download_count: Int? = null,
|
||||||
|
val created_at: String? = null,
|
||||||
|
val updated_at: String? = null,
|
||||||
|
val browser_download_url: String? = null,
|
||||||
|
)
|
|
@ -4,6 +4,7 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import me.ash.reader.data.source.AppNetworkDataSource
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
@ -13,6 +14,11 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object RetrofitModule {
|
object RetrofitModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAppNetworkDataSource(): AppNetworkDataSource =
|
||||||
|
AppNetworkDataSource.getInstance()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRssNetworkDataSource(): RssNetworkDataSource =
|
fun provideRssNetworkDataSource(): RssNetworkDataSource =
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
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 javax.inject.Inject
|
||||||
|
|
||||||
|
class AppRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val appNetworkDataSource: AppNetworkDataSource,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
@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()
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
import me.ash.reader.data.entity.GitHubRelease
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
interface AppNetworkDataSource {
|
||||||
|
@GET("https://api.github.com/repos/Ashinch/ReadYou/releases/latest")
|
||||||
|
suspend fun getReleaseLatest(): GitHubRelease
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: AppNetworkDataSource? = null
|
||||||
|
|
||||||
|
fun getInstance(): AppNetworkDataSource {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: Retrofit.Builder()
|
||||||
|
.baseUrl("https://api.github.com/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build().create(AppNetworkDataSource::class.java).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ fun Banner(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String,
|
title: String,
|
||||||
desc: String? = null,
|
desc: String? = null,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
action: (@Composable () -> Unit)? = null,
|
action: (@Composable () -> Unit)? = null,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
|
@ -46,7 +47,7 @@ fun Banner(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clip(RoundedCornerShape(32.dp))
|
.clip(RoundedCornerShape(32.dp))
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer alwaysLight true)
|
.background(backgroundColor alwaysLight true)
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(16.dp, 20.dp),
|
.padding(16.dp, 20.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
|
@ -2,14 +2,16 @@ package me.ash.reader.ui.component
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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.platform.LocalView
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedbackIconButton(
|
fun FeedbackIconButton(
|
||||||
|
@ -17,6 +19,7 @@ fun FeedbackIconButton(
|
||||||
imageVector: ImageVector,
|
imageVector: ImageVector,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
tint: Color = LocalContentColor.current,
|
tint: Color = LocalContentColor.current,
|
||||||
|
showBadge: Boolean = false,
|
||||||
isHaptic: Boolean? = true,
|
isHaptic: Boolean? = true,
|
||||||
isSound: Boolean? = true,
|
isSound: Boolean? = true,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
|
@ -30,11 +33,32 @@ fun FeedbackIconButton(
|
||||||
onClick()
|
onClick()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
if (showBadge) {
|
||||||
modifier = modifier,
|
BadgedBox(
|
||||||
imageVector = imageVector,
|
badge = {
|
||||||
contentDescription = contentDescription,
|
Badge(
|
||||||
tint = tint,
|
modifier = Modifier
|
||||||
)
|
.size(8.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = modifier,
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
modifier = modifier,
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -12,6 +12,19 @@ import kotlinx.coroutines.runBlocking
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
|
||||||
|
val Context.newVersionPublishDate: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.NewVersionPublishDate) ?: ""
|
||||||
|
val Context.newVersionLog: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.NewVersionLog) ?: ""
|
||||||
|
val Context.newVersionSize: Int
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.NewVersionSize) ?: 0
|
||||||
|
val Context.newVersionDownloadUrl: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.NewVersionDownloadUrl) ?: ""
|
||||||
|
val Context.newVersionNumber: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.NewVersionNumber) ?: ""
|
||||||
|
val Context.skipVersionNumber: String
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.SkipVersionNumber) ?: ""
|
||||||
val Context.isFirstLaunch: Boolean
|
val Context.isFirstLaunch: Boolean
|
||||||
get() = this.dataStore.get(DataStoreKeys.IsFirstLaunch) ?: true
|
get() = this.dataStore.get(DataStoreKeys.IsFirstLaunch) ?: true
|
||||||
val Context.currentAccountId: Int
|
val Context.currentAccountId: Int
|
||||||
|
@ -62,6 +75,36 @@ sealed class DataStoreKeys<T> {
|
||||||
get() = booleanPreferencesKey("isFirstLaunch")
|
get() = booleanPreferencesKey("isFirstLaunch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object NewVersionPublishDate : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("newVersionPublishDate")
|
||||||
|
}
|
||||||
|
|
||||||
|
object NewVersionLog : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("newVersionLog")
|
||||||
|
}
|
||||||
|
|
||||||
|
object NewVersionSize : DataStoreKeys<Int>() {
|
||||||
|
override val key: Preferences.Key<Int>
|
||||||
|
get() = intPreferencesKey("newVersionSize")
|
||||||
|
}
|
||||||
|
|
||||||
|
object NewVersionDownloadUrl : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("newVersionDownloadUrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
object NewVersionNumber : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("newVersionNumber")
|
||||||
|
}
|
||||||
|
|
||||||
|
object SkipVersionNumber : DataStoreKeys<String>() {
|
||||||
|
override val key: Preferences.Key<String>
|
||||||
|
get() = stringPreferencesKey("skipVersionNumber")
|
||||||
|
}
|
||||||
|
|
||||||
object CurrentAccountId : DataStoreKeys<Int>() {
|
object CurrentAccountId : DataStoreKeys<Int>() {
|
||||||
override val key: Preferences.Key<Int>
|
override val key: Preferences.Key<Int>
|
||||||
get() = intPreferencesKey("currentAccountId")
|
get() = intPreferencesKey("currentAccountId")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
|
@ -26,15 +27,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
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
|
||||||
import me.ash.reader.ui.component.Subtitle
|
import me.ash.reader.ui.component.Subtitle
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.*
|
||||||
import me.ash.reader.ui.ext.getDesc
|
|
||||||
import me.ash.reader.ui.ext.getName
|
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
import me.ash.reader.ui.page.common.RouteName
|
||||||
import me.ash.reader.ui.page.home.FilterBar
|
import me.ash.reader.ui.page.home.FilterBar
|
||||||
import me.ash.reader.ui.page.home.FilterState
|
import me.ash.reader.ui.page.home.FilterState
|
||||||
|
@ -42,6 +44,7 @@ import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
|
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
|
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
|
ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
|
||||||
androidx.compose.foundation.ExperimentalFoundationApi::class
|
androidx.compose.foundation.ExperimentalFoundationApi::class
|
||||||
|
@ -59,6 +62,19 @@ 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 owner = LocalLifecycleOwner.current
|
val owner = LocalLifecycleOwner.current
|
||||||
|
@ -116,6 +132,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,
|
||||||
) {
|
) {
|
||||||
navController.navigate(RouteName.SETTINGS)
|
navController.navigate(RouteName.SETTINGS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package me.ash.reader.ui.page.settings
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import android.view.SoundEffectConstants
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -25,6 +26,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@ -38,8 +40,15 @@ fun SelectableSettingGroupItem(
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
|
modifier = modifier
|
||||||
|
.clickable {
|
||||||
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
.alpha(if (enable) 1f else 0.5f),
|
||||||
color = Color.Unspecified,
|
color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
@ -1,33 +1,54 @@
|
||||||
package me.ash.reader.ui.page.settings
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.compose.ui.zIndex
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
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.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.dataStore
|
||||||
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
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsPage(
|
fun SettingsPage(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
) {
|
) {
|
||||||
|
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()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
|
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
|
||||||
|
@ -56,17 +77,30 @@ fun SettingsPage(
|
||||||
DisplayText(text = stringResource(R.string.settings), desc = "")
|
DisplayText(text = stringResource(R.string.settings), desc = "")
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Banner(
|
Box {
|
||||||
title = stringResource(R.string.in_coding),
|
if (newVersionNumber > skipVersionNumber) {
|
||||||
desc = stringResource(R.string.coming_soon),
|
Banner(
|
||||||
icon = Icons.Outlined.Lightbulb,
|
modifier = Modifier.zIndex(1f),
|
||||||
action = {
|
title = stringResource(R.string.get_new_updates),
|
||||||
Icon(
|
desc = stringResource(
|
||||||
imageVector = Icons.Rounded.Close,
|
R.string.get_new_updates_desc,
|
||||||
contentDescription = stringResource(R.string.close),
|
newVersionNumber.joinToString(".")
|
||||||
)
|
),
|
||||||
},
|
icon = Icons.Outlined.Lightbulb,
|
||||||
)
|
action = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = stringResource(R.string.close),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { updateDialogVisible = true }
|
||||||
|
}
|
||||||
|
Banner(
|
||||||
|
title = stringResource(R.string.in_coding),
|
||||||
|
desc = stringResource(R.string.coming_soon),
|
||||||
|
icon = Icons.Outlined.Lightbulb,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
@ -114,4 +148,9 @@ fun SettingsPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UpdateDialog(
|
||||||
|
visible = updateDialogVisible,
|
||||||
|
onDismissRequest = { updateDialogVisible = false },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
115
app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt
Normal file
115
app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Update
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.component.Dialog
|
||||||
|
import me.ash.reader.ui.ext.DataStoreKeys
|
||||||
|
import me.ash.reader.ui.ext.dataStore
|
||||||
|
import me.ash.reader.ui.ext.put
|
||||||
|
|
||||||
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun UpdateDialog(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
visible: Boolean = false,
|
||||||
|
onDismissRequest: () -> Unit = {},
|
||||||
|
onConfirm: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
val newVersionNumber = context.dataStore.data
|
||||||
|
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
|
||||||
|
.collectAsState(initial = "")
|
||||||
|
.value
|
||||||
|
val newVersionPublishDate = context.dataStore.data
|
||||||
|
.map { it[DataStoreKeys.NewVersionPublishDate.key] ?: "" }
|
||||||
|
.collectAsState(initial = "")
|
||||||
|
.value
|
||||||
|
val newVersionLog = context.dataStore.data
|
||||||
|
.map { it[DataStoreKeys.NewVersionLog.key] ?: "" }
|
||||||
|
.collectAsState(initial = "")
|
||||||
|
.value
|
||||||
|
val newVersionSize = context.dataStore.data
|
||||||
|
.map { it[DataStoreKeys.NewVersionSize.key] ?: 0 }
|
||||||
|
.map { it / 1024f / 1024f }
|
||||||
|
.collectAsState(initial = 0)
|
||||||
|
.value
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
modifier = modifier.heightIn(max = 400.dp),
|
||||||
|
visible = visible,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Update,
|
||||||
|
contentDescription = stringResource(R.string.change_log),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.change_log))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = newVersionPublishDate,
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
text = newVersionLog,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.update, String.format("%.2f", newVersionSize)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.skip_this_version))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -73,7 +73,7 @@
|
||||||
<string name="seven_days">7天</string>
|
<string name="seven_days">7天</string>
|
||||||
<string name="close">关闭</string>
|
<string name="close">关闭</string>
|
||||||
<string name="get_new_updates">获取新的更新</string>
|
<string name="get_new_updates">获取新的更新</string>
|
||||||
<string name="get_new_updates_desc">版本 0.6.1 现已发布</string>
|
<string name="get_new_updates_desc">版本 %1$s 现已发布</string>
|
||||||
<string name="in_coding">施工中</string>
|
<string name="in_coding">施工中</string>
|
||||||
<string name="coming_soon">正在路上</string>
|
<string name="coming_soon">正在路上</string>
|
||||||
<string name="accounts">账户</string>
|
<string name="accounts">账户</string>
|
||||||
|
@ -111,4 +111,8 @@
|
||||||
<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="change_log">更新日志</string>
|
||||||
|
<string name="update">更新 %1$s MB</string>
|
||||||
|
<string name="skip_this_version">跳过这个版本</string>
|
||||||
</resources>
|
</resources>
|
|
@ -73,7 +73,7 @@
|
||||||
<string name="seven_days">7d</string>
|
<string name="seven_days">7d</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="get_new_updates">Get new updates</string>
|
<string name="get_new_updates">Get new updates</string>
|
||||||
<string name="get_new_updates_desc">Version 0.6.1 has been released</string>
|
<string name="get_new_updates_desc">Version %1$s has been released</string>
|
||||||
<string name="in_coding">In coding</string>
|
<string name="in_coding">In coding</string>
|
||||||
<string name="coming_soon">Coming soon</string>
|
<string name="coming_soon">Coming soon</string>
|
||||||
<string name="accounts">Accounts</string>
|
<string name="accounts">Accounts</string>
|
||||||
|
@ -111,4 +111,8 @@
|
||||||
<string name="open_source_licenses">Open Source Licenses</string>
|
<string name="open_source_licenses">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="change_log">Change Log</string>
|
||||||
|
<string name="update">Update %1$s MB</string>
|
||||||
|
<string name="skip_this_version">Skip This Version</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user