Add download APK file
This commit is contained in:
parent
aa6517818c
commit
840970bfe2
|
@ -21,8 +21,8 @@ android {
|
||||||
applicationId "me.ash.reader"
|
applicationId "me.ash.reader"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 32
|
targetSdk 32
|
||||||
versionCode 5
|
versionCode 6
|
||||||
versionName "0.7.2"
|
versionName "0.7.4"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package="me.ash.reader">
|
package="me.ash.reader">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
@ -24,10 +25,21 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
tools:node="remove"></provider>
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -15,9 +15,7 @@ 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
|
||||||
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 javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
|
@ -99,7 +97,12 @@ class App : Application(), Configuration.Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkUpdate() {
|
private suspend fun checkUpdate() {
|
||||||
appRepository.checkUpdate()
|
applicationContext.getLatestApk().let {
|
||||||
|
if (it.exists()) {
|
||||||
|
it.del()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appRepository.checkUpdate(showToast = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration =
|
override fun getWorkManagerConfiguration(): Configuration =
|
||||||
|
|
|
@ -5,12 +5,17 @@ import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.toVersion
|
import me.ash.reader.data.entity.toVersion
|
||||||
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.module.DispatcherMain
|
||||||
import me.ash.reader.data.source.AppNetworkDataSource
|
import me.ash.reader.data.source.AppNetworkDataSource
|
||||||
|
import me.ash.reader.data.source.Download
|
||||||
|
import me.ash.reader.data.source.downloadToFileWithProgress
|
||||||
import me.ash.reader.ui.ext.*
|
import me.ash.reader.ui.ext.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -22,11 +27,28 @@ class AppRepository @Inject constructor(
|
||||||
private val applicationScope: CoroutineScope,
|
private val applicationScope: CoroutineScope,
|
||||||
@DispatcherIO
|
@DispatcherIO
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
|
@DispatcherMain
|
||||||
|
private val dispatcherMain: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
suspend fun checkUpdate(): Boolean = withContext(dispatcherIO) {
|
suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val latest =
|
val response =
|
||||||
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
|
||||||
|
when {
|
||||||
|
response.code() == 403 -> {
|
||||||
|
withContext(dispatcherMain) {
|
||||||
|
if (showToast) context.showToast(context.getString(R.string.rate_limit))
|
||||||
|
}
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
response.body() == null -> {
|
||||||
|
withContext(dispatcherMain) {
|
||||||
|
if (showToast) context.showToast(context.getString(R.string.check_failure))
|
||||||
|
}
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val latest = response.body()!!
|
||||||
val latestVersion = latest.tag_name.toVersion()
|
val latestVersion = latest.tag_name.toVersion()
|
||||||
// val latestVersion = "0.7.3".toVersion()
|
// val latestVersion = "0.7.3".toVersion()
|
||||||
val skipVersion = context.skipVersionNumber.toVersion()
|
val skipVersion = context.skipVersionNumber.toVersion()
|
||||||
|
@ -60,7 +82,26 @@ class AppRepository @Inject constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Log.e("RLog", "checkUpdate: ${e.message}")
|
Log.e("RLog", "checkUpdate: ${e.message}")
|
||||||
false
|
withContext(dispatcherMain) {
|
||||||
|
if (showToast) context.showToast(context.getString(R.string.check_failure))
|
||||||
|
}
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun downloadFile(url: String): Flow<Download> =
|
||||||
|
withContext(dispatcherIO) {
|
||||||
|
Log.i("RLog", "downloadFile start: $url")
|
||||||
|
try {
|
||||||
|
return@withContext appNetworkDataSource.downloadFile(url)
|
||||||
|
.downloadToFileWithProgress(context.getLatestApk())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.e("RLog", "downloadFile: ${e.message}")
|
||||||
|
withContext(dispatcherMain) {
|
||||||
|
context.showToast(context.getString(R.string.download_failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyFlow()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,14 +1,33 @@
|
||||||
package me.ash.reader.data.source
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import me.ash.reader.data.entity.LatestRelease
|
import me.ash.reader.data.entity.LatestRelease
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
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.Streaming
|
||||||
import retrofit2.http.Url
|
import retrofit2.http.Url
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
sealed class Download {
|
||||||
|
object NotYet : Download()
|
||||||
|
data class Progress(val percent: Int) : Download()
|
||||||
|
data class Finished(val file: File) : Download()
|
||||||
|
}
|
||||||
|
|
||||||
interface AppNetworkDataSource {
|
interface AppNetworkDataSource {
|
||||||
@GET
|
@GET
|
||||||
suspend fun getReleaseLatest(@Url url: String): LatestRelease
|
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Streaming
|
||||||
|
suspend fun downloadFile(@Url url: String): ResponseBody
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: AppNetworkDataSource? = null
|
private var instance: AppNetworkDataSource? = null
|
||||||
|
@ -25,3 +44,52 @@ interface AppNetworkDataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
|
||||||
|
flow {
|
||||||
|
emit(Download.Progress(0))
|
||||||
|
|
||||||
|
// flag to delete file if download errors or is cancelled
|
||||||
|
var deleteFile = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
byteStream().use { inputStream ->
|
||||||
|
saveFile.outputStream().use { outputStream ->
|
||||||
|
val totalBytes = contentLength()
|
||||||
|
val data = ByteArray(8_192)
|
||||||
|
var progressBytes = 0L
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val bytes = inputStream.read(data)
|
||||||
|
|
||||||
|
if (bytes == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.channel
|
||||||
|
outputStream.write(data, 0, bytes)
|
||||||
|
progressBytes += bytes
|
||||||
|
|
||||||
|
emit(Download.Progress(percent = ((progressBytes * 100) / totalBytes).toInt()))
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
progressBytes < totalBytes ->
|
||||||
|
throw Exception("missing bytes")
|
||||||
|
progressBytes > totalBytes ->
|
||||||
|
throw Exception("too many bytes")
|
||||||
|
else ->
|
||||||
|
deleteFile = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(Download.Finished(saveFile))
|
||||||
|
} finally {
|
||||||
|
// check if download was successful
|
||||||
|
|
||||||
|
if (deleteFile) {
|
||||||
|
saveFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO).distinctUntilChanged()
|
|
@ -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 android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import me.ash.reader.data.entity.Version
|
import me.ash.reader.data.entity.Version
|
||||||
import me.ash.reader.data.entity.toVersion
|
import me.ash.reader.data.entity.toVersion
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
fun Context.findActivity(): Activity? = when (this) {
|
fun Context.findActivity(): Activity? = when (this) {
|
||||||
is Activity -> this
|
is Activity -> this
|
||||||
|
@ -18,6 +22,27 @@ fun Context.getCurrentVersion(): Version = packageManager
|
||||||
.versionName
|
.versionName
|
||||||
.toVersion()
|
.toVersion()
|
||||||
|
|
||||||
|
fun Context.getLatestApk(): File = File(cacheDir, "latest.apk")
|
||||||
|
|
||||||
|
fun Context.getFileProvider(): String = "${packageName}.fileprovider"
|
||||||
|
|
||||||
|
fun Context.installLatestApk() {
|
||||||
|
try {
|
||||||
|
val contentUri = FileProvider.getUriForFile(this, getFileProvider(), getLatestApk())
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
setDataAndType(contentUri, "application/vnd.android.package-archive")
|
||||||
|
}
|
||||||
|
if (packageManager.queryIntentActivities(intent, 0).size > 0) {
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.e("RLog", "installLatestApk: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var toast: Toast? = null
|
private var toast: Toast? = null
|
||||||
|
|
||||||
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
|
|
38
app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
Normal file
38
app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package me.ash.reader.ui.ext
|
||||||
|
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert [InputStream] to [String].
|
||||||
|
*/
|
||||||
|
fun InputStream.readString(): String =
|
||||||
|
BufferedReader(InputStreamReader(this)).useLines { lines ->
|
||||||
|
val results = StringBuilder()
|
||||||
|
lines.forEach { results.append(it) }
|
||||||
|
results.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file or directory.
|
||||||
|
*/
|
||||||
|
fun File.del() {
|
||||||
|
if (this.isFile) {
|
||||||
|
delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.listFiles()?.forEach { it.del() }
|
||||||
|
delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.mkDir() {
|
||||||
|
val dirArray = this.absolutePath.split("/".toRegex())
|
||||||
|
var pathTemp = ""
|
||||||
|
for (i in 1 until dirArray.size) {
|
||||||
|
pathTemp = "$pathTemp/${dirArray[i]}"
|
||||||
|
val newF = File("${dirArray[0]}$pathTemp")
|
||||||
|
if (!newF.exists()) newF.mkdir()
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ 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.compose.ui.zIndex
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
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
|
||||||
|
@ -33,9 +34,9 @@ import me.ash.reader.ui.theme.palette.onLight
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsPage(
|
fun SettingsPage(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
|
updateViewModel: UpdateViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var updateDialogVisible by remember { mutableStateOf(false) }
|
|
||||||
val skipVersion = context.dataStore.data
|
val skipVersion = context.dataStore.data
|
||||||
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
|
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
|
||||||
.collectAsState(initial = "")
|
.collectAsState(initial = "")
|
||||||
|
@ -92,7 +93,9 @@ fun SettingsPage(
|
||||||
contentDescription = stringResource(R.string.close),
|
contentDescription = stringResource(R.string.close),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { updateDialogVisible = true }
|
) {
|
||||||
|
updateViewModel.dispatch(UpdateViewAction.Show)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Banner(
|
Banner(
|
||||||
title = stringResource(R.string.in_coding),
|
title = stringResource(R.string.in_coding),
|
||||||
|
@ -148,8 +151,5 @@ fun SettingsPage(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
UpdateDialog(
|
UpdateDialog()
|
||||||
visible = updateDialogVisible,
|
|
||||||
onDismissRequest = { updateDialogVisible = false },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.Balance
|
import androidx.compose.material.icons.rounded.Balance
|
||||||
|
import androidx.compose.material.icons.rounded.TipsAndUpdates
|
||||||
import androidx.compose.material.icons.rounded.VolunteerActivism
|
import androidx.compose.material.icons.rounded.VolunteerActivism
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
@ -30,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
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.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
@ -50,13 +52,8 @@ fun TipsAndSupport(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val viewState = updateViewModel.viewState.collectAsStateValue()
|
|
||||||
val githubLink = stringResource(R.string.github_link)
|
|
||||||
val telegramLink = stringResource(R.string.telegram_link)
|
|
||||||
val checkingUpdates = stringResource(R.string.checking_updates)
|
|
||||||
val isLatestVersion = stringResource(R.string.is_latest_version)
|
|
||||||
val comingSoon = stringResource(R.string.coming_soon)
|
|
||||||
var currentVersion by remember { mutableStateOf("") }
|
var currentVersion by remember { mutableStateOf("") }
|
||||||
|
var clickTime by remember { mutableStateOf(System.currentTimeMillis() - 2000) }
|
||||||
var pressAMP by remember { mutableStateOf(16f) }
|
var pressAMP by remember { mutableStateOf(16f) }
|
||||||
val animatedPress by animateFloatAsState(
|
val animatedPress by animateFloatAsState(
|
||||||
targetValue = pressAMP,
|
targetValue = pressAMP,
|
||||||
|
@ -88,7 +85,16 @@ fun TipsAndSupport(
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {}
|
actions = {
|
||||||
|
FeedbackIconButton(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = Icons.Rounded.Balance,
|
||||||
|
contentDescription = stringResource(R.string.open_source_licenses),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
context.showToast(context.getString(R.string.coming_soon))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
|
@ -110,21 +116,30 @@ fun TipsAndSupport(
|
||||||
pressAMP = 16f
|
pressAMP = 16f
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
context.showToast(checkingUpdates)
|
if (System.currentTimeMillis() - clickTime > 2000) {
|
||||||
scope.launch {
|
clickTime = System.currentTimeMillis()
|
||||||
updateViewModel.dispatch(
|
context.showToast(context.getString(R.string.checking_updates))
|
||||||
UpdateViewAction.CheckUpdate(
|
scope.launch {
|
||||||
{
|
updateViewModel.dispatch(
|
||||||
context.dataStore.put(
|
UpdateViewAction.CheckUpdate(
|
||||||
DataStoreKeys.SkipVersionNumber,
|
{
|
||||||
""
|
context.dataStore.put(
|
||||||
)
|
DataStoreKeys.SkipVersionNumber,
|
||||||
},
|
""
|
||||||
{
|
)
|
||||||
if (!it) context.showToast(isLatestVersion)
|
},
|
||||||
}
|
{
|
||||||
|
if (!it) {
|
||||||
|
context.showToast(
|
||||||
|
context.getString(R.string.is_latest_version)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
} else {
|
||||||
|
clickTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -186,22 +201,7 @@ fun TipsAndSupport(
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
context.showToast(comingSoon)
|
context.showToast(context.getString(R.string.coming_soon))
|
||||||
})
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
// Telegram
|
|
||||||
RoundIconButton(RoundIconButtonType.Telegram(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true,
|
|
||||||
) {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
context.startActivity(
|
|
||||||
Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
Uri.parse(telegramLink)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
@ -214,19 +214,34 @@ fun TipsAndSupport(
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
Uri.parse(githubLink)
|
Uri.parse(context.getString(R.string.github_link))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
// License
|
// Telegram
|
||||||
RoundIconButton(RoundIconButtonType.License(
|
RoundIconButton(RoundIconButtonType.Telegram(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true,
|
||||||
|
) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse(context.getString(R.string.telegram_link))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// Help
|
||||||
|
RoundIconButton(RoundIconButtonType.Help(
|
||||||
backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true,
|
backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true,
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
context.showToast(comingSoon)
|
context.showToast(context.getString(R.string.coming_soon))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
@ -235,10 +250,7 @@ fun TipsAndSupport(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
UpdateDialog(
|
UpdateDialog()
|
||||||
visible = viewState.updateDialogVisible,
|
|
||||||
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -247,6 +259,7 @@ sealed class RoundIconButtonType(
|
||||||
val iconVector: ImageVector? = null,
|
val iconVector: ImageVector? = null,
|
||||||
val descResource: Int? = null,
|
val descResource: Int? = null,
|
||||||
val descString: String? = null,
|
val descString: String? = null,
|
||||||
|
open val size: Dp = 24.dp,
|
||||||
open val offset: Modifier = Modifier.offset(),
|
open val offset: Modifier = Modifier.offset(),
|
||||||
open val backgroundColor: Color = Color.Unspecified,
|
open val backgroundColor: Color = Color.Unspecified,
|
||||||
open val onClick: () -> Unit = {},
|
open val onClick: () -> Unit = {},
|
||||||
|
@ -263,6 +276,18 @@ sealed class RoundIconButtonType(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class GitHub(
|
||||||
|
val desc: String = "GitHub",
|
||||||
|
override val backgroundColor: Color,
|
||||||
|
override val onClick: () -> Unit = {},
|
||||||
|
) : RoundIconButtonType(
|
||||||
|
iconResource = R.drawable.ic_github,
|
||||||
|
descString = desc,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Telegram(
|
data class Telegram(
|
||||||
val desc: String = "Telegram",
|
val desc: String = "Telegram",
|
||||||
|
@ -277,24 +302,13 @@ sealed class RoundIconButtonType(
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class GitHub(
|
data class Help(
|
||||||
val desc: String = "GitHub",
|
val desc: Int = R.string.help,
|
||||||
|
override val offset: Modifier = Modifier.offset(x = (3).dp),
|
||||||
override val backgroundColor: Color,
|
override val backgroundColor: Color,
|
||||||
override val onClick: () -> Unit = {},
|
override val onClick: () -> Unit = {},
|
||||||
) : RoundIconButtonType(
|
) : RoundIconButtonType(
|
||||||
iconResource = R.drawable.ic_github,
|
iconVector = Icons.Rounded.TipsAndUpdates,
|
||||||
descString = desc,
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class License(
|
|
||||||
val desc: Int = R.string.open_source_licenses,
|
|
||||||
override val backgroundColor: Color,
|
|
||||||
override val onClick: () -> Unit = {},
|
|
||||||
) : RoundIconButtonType(
|
|
||||||
iconVector = Icons.Rounded.Balance,
|
|
||||||
descResource = desc,
|
descResource = desc,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
@ -313,9 +327,9 @@ private fun RoundIconButton(type: RoundIconButtonType) {
|
||||||
onClick = { type.onClick() }
|
onClick = { type.onClick() }
|
||||||
) {
|
) {
|
||||||
when (type) {
|
when (type) {
|
||||||
is RoundIconButtonType.Sponsor, is RoundIconButtonType.License -> {
|
is RoundIconButtonType.Sponsor, is RoundIconButtonType.Help -> {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = type.offset,
|
modifier = type.offset.size(type.size),
|
||||||
imageVector = type.iconVector!!,
|
imageVector = type.iconVector!!,
|
||||||
contentDescription = stringResource(type.descResource!!),
|
contentDescription = stringResource(type.descResource!!),
|
||||||
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
|
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
|
||||||
|
@ -323,7 +337,7 @@ private fun RoundIconButton(type: RoundIconButtonType) {
|
||||||
}
|
}
|
||||||
is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> {
|
is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = type.offset,
|
modifier = type.offset.size(type.size),
|
||||||
painter = painterResource(type.iconResource!!),
|
painter = painterResource(type.iconResource!!),
|
||||||
contentDescription = type.descString,
|
contentDescription = type.descString,
|
||||||
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
|
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
package me.ash.reader.ui.page.settings
|
package me.ash.reader.ui.page.settings
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
@ -22,26 +29,25 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.hilt.navigation.compose.hiltViewModel
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.data.source.Download
|
||||||
import me.ash.reader.ui.component.Dialog
|
import me.ash.reader.ui.component.Dialog
|
||||||
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
|
|
||||||
|
|
||||||
@SuppressLint("FlowOperatorInvokedInComposition")
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateDialog(
|
fun UpdateDialog(
|
||||||
modifier: Modifier = Modifier,
|
updateViewModel: UpdateViewModel = hiltViewModel(),
|
||||||
visible: Boolean = false,
|
|
||||||
onDismissRequest: () -> Unit = {},
|
|
||||||
onConfirm: (String) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val viewState = updateViewModel.viewState.collectAsStateValue()
|
||||||
|
val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val newVersionNumber = context.dataStore.data
|
val newVersionNumber = context.dataStore.data
|
||||||
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
|
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
|
||||||
|
@ -55,17 +61,38 @@ fun UpdateDialog(
|
||||||
.map { it[DataStoreKeys.NewVersionLog.key] ?: "" }
|
.map { it[DataStoreKeys.NewVersionLog.key] ?: "" }
|
||||||
.collectAsState(initial = "")
|
.collectAsState(initial = "")
|
||||||
.value
|
.value
|
||||||
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 "" }
|
.map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" }
|
||||||
.collectAsState(initial = 0)
|
.collectAsState(initial = 0)
|
||||||
.value
|
.value
|
||||||
|
|
||||||
|
val settings = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
context.installLatestApk()
|
||||||
|
}
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { result ->
|
||||||
|
if (result) {
|
||||||
|
context.installLatestApk()
|
||||||
|
} else {
|
||||||
|
settings.launch(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
|
Uri.parse("package:${context.packageName}"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
modifier = modifier.heightIn(max = 400.dp),
|
modifier = Modifier.heightIn(max = 400.dp),
|
||||||
visible = visible,
|
visible = viewState.updateDialogVisible,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Update,
|
imageVector = Icons.Rounded.Update,
|
||||||
|
@ -79,7 +106,7 @@ fun UpdateDialog(
|
||||||
Text(text = stringResource(R.string.change_log))
|
Text(text = stringResource(R.string.change_log))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = newVersionPublishDate,
|
text = "$newVersionPublishDate$newVersionSize",
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
@ -97,19 +124,47 @@ fun UpdateDialog(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
if (downloadState !is Download.Progress) {
|
||||||
|
updateViewModel.dispatch(
|
||||||
|
UpdateViewAction.DownloadUpdate(
|
||||||
|
url = context.newVersionDownloadUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.update) + newVersionSize)
|
Text(
|
||||||
|
text = stringResource(R.string.update) + when (downloadState) {
|
||||||
|
is Download.NotYet -> ""
|
||||||
|
is Download.Progress -> " ${downloadState.percent}%"
|
||||||
|
is Download.Finished -> {
|
||||||
|
if (context.packageManager.canRequestPackageInstalls()) {
|
||||||
|
Log.i(
|
||||||
|
"RLog",
|
||||||
|
"Download.Finished: ${downloadState.file.absolutePath}"
|
||||||
|
)
|
||||||
|
context.installLatestApk()
|
||||||
|
} else {
|
||||||
|
launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES)
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = {
|
if (downloadState !is Download.Progress) {
|
||||||
scope.launch {
|
TextButton(
|
||||||
context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
|
onClick = {
|
||||||
onDismissRequest()
|
scope.launch {
|
||||||
|
context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
|
||||||
|
updateViewModel.dispatch(UpdateViewAction.Hide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.skip_this_version))
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Text(text = stringResource(R.string.skip_this_version))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,12 +3,10 @@ package me.ash.reader.ui.page.settings
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.repository.AppRepository
|
import me.ash.reader.data.repository.AppRepository
|
||||||
|
import me.ash.reader.data.source.Download
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -26,6 +24,7 @@ class UpdateViewModel @Inject constructor(
|
||||||
action.preProcessor,
|
action.preProcessor,
|
||||||
action.postProcessor
|
action.postProcessor
|
||||||
)
|
)
|
||||||
|
is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +35,10 @@ class UpdateViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preProcessor()
|
preProcessor()
|
||||||
appRepository.checkUpdate().let {
|
appRepository.checkUpdate().let {
|
||||||
changeUpdateDialogVisible(it)
|
it?.let {
|
||||||
postProcessor(it)
|
changeUpdateDialogVisible(it)
|
||||||
|
postProcessor(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,10 +50,26 @@ class UpdateViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadUpdate(url: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
downloadFlow = flow { emit(Download.Progress(0)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
downloadFlow = appRepository.downloadFile(url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpdateViewState(
|
data class UpdateViewState(
|
||||||
val updateDialogVisible: Boolean = false,
|
val updateDialogVisible: Boolean = false,
|
||||||
|
val downloadFlow: Flow<Download> = emptyFlow(),
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class UpdateViewAction {
|
sealed class UpdateViewAction {
|
||||||
|
@ -63,4 +80,8 @@ sealed class UpdateViewAction {
|
||||||
val preProcessor: suspend () -> Unit = {},
|
val preProcessor: suspend () -> Unit = {},
|
||||||
val postProcessor: suspend (Boolean) -> Unit = {}
|
val postProcessor: suspend (Boolean) -> Unit = {}
|
||||||
) : UpdateViewAction()
|
) : UpdateViewAction()
|
||||||
|
|
||||||
|
data class DownloadUpdate(
|
||||||
|
val url: String,
|
||||||
|
) : UpdateViewAction()
|
||||||
}
|
}
|
|
@ -117,4 +117,8 @@
|
||||||
<string name="skip_this_version">跳过这个版本</string>
|
<string name="skip_this_version">跳过这个版本</string>
|
||||||
<string name="checking_updates">正在检查更新…</string>
|
<string name="checking_updates">正在检查更新…</string>
|
||||||
<string name="is_latest_version">已是最新版本</string>
|
<string name="is_latest_version">已是最新版本</string>
|
||||||
|
<string name="check_failure">检查失败</string>
|
||||||
|
<string name="download_failure">下载失败</string>
|
||||||
|
<string name="rate_limit">请求速率受限</string>
|
||||||
|
<string name="help">帮助</string>
|
||||||
</resources>
|
</resources>
|
|
@ -117,4 +117,8 @@
|
||||||
<string name="skip_this_version">Skip This Version</string>
|
<string name="skip_this_version">Skip This Version</string>
|
||||||
<string name="checking_updates">Checking for updates…</string>
|
<string name="checking_updates">Checking for updates…</string>
|
||||||
<string name="is_latest_version">This is the latest version</string>
|
<string name="is_latest_version">This is the latest version</string>
|
||||||
|
<string name="check_failure">Check failure</string>
|
||||||
|
<string name="download_failure">Download failure</string>
|
||||||
|
<string name="rate_limit">The request rate is limited</string>
|
||||||
|
<string name="help">Help</string>
|
||||||
</resources>
|
</resources>
|
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="cache" path="."/>
|
||||||
|
</paths>
|
Loading…
Reference in New Issue
Block a user