Add download APK file

This commit is contained in:
Ash 2022-04-25 02:21:29 +08:00
parent aa6517818c
commit 840970bfe2
14 changed files with 395 additions and 106 deletions

View File

@ -21,8 +21,8 @@ android {
applicationId "me.ash.reader"
minSdk 26
targetSdk 32
versionCode 5
versionName "0.7.2"
versionCode 6
versionName "0.7.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -4,6 +4,7 @@
package="me.ash.reader">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".App"
@ -24,10 +25,21 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
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>
</manifest>

View File

@ -15,9 +15,7 @@ import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource
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.*
import javax.inject.Inject
@HiltAndroidApp
@ -99,7 +97,12 @@ class App : Application(), Configuration.Provider {
}
private suspend fun checkUpdate() {
appRepository.checkUpdate()
applicationContext.getLatestApk().let {
if (it.exists()) {
it.del()
}
}
appRepository.checkUpdate(showToast = false)
}
override fun getWorkManagerConfiguration(): Configuration =

View File

@ -5,12 +5,17 @@ import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import me.ash.reader.R
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.module.ApplicationScope
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.Download
import me.ash.reader.data.source.downloadToFileWithProgress
import me.ash.reader.ui.ext.*
import javax.inject.Inject
@ -22,11 +27,28 @@ class AppRepository @Inject constructor(
private val applicationScope: CoroutineScope,
@DispatcherIO
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 {
val latest =
val response =
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 = "0.7.3".toVersion()
val skipVersion = context.skipVersionNumber.toVersion()
@ -60,7 +82,26 @@ class AppRepository @Inject constructor(
} catch (e: Exception) {
e.printStackTrace()
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()
}
}

View File

@ -1,14 +1,33 @@
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 okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Streaming
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 {
@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 {
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()

View File

@ -3,9 +3,13 @@ package me.ash.reader.ui.ext
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import me.ash.reader.data.entity.Version
import me.ash.reader.data.entity.toVersion
import java.io.File
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
@ -18,6 +22,27 @@ fun Context.getCurrentVersion(): Version = packageManager
.versionName
.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
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {

View 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()
}
}

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map
import me.ash.reader.R
@ -33,9 +34,9 @@ import me.ash.reader.ui.theme.palette.onLight
@Composable
fun SettingsPage(
navController: NavHostController,
updateViewModel: UpdateViewModel = hiltViewModel(),
) {
val context = LocalContext.current
var updateDialogVisible by remember { mutableStateOf(false) }
val skipVersion = context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
.collectAsState(initial = "")
@ -92,7 +93,9 @@ fun SettingsPage(
contentDescription = stringResource(R.string.close),
)
},
) { updateDialogVisible = true }
) {
updateViewModel.dispatch(UpdateViewAction.Show)
}
}
Banner(
title = stringResource(R.string.in_coding),
@ -148,8 +151,5 @@ fun SettingsPage(
}
)
UpdateDialog(
visible = updateDialogVisible,
onDismissRequest = { updateDialogVisible = false },
)
UpdateDialog()
}

View File

@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Balance
import androidx.compose.material.icons.rounded.TipsAndUpdates
import androidx.compose.material.icons.rounded.VolunteerActivism
import androidx.compose.material3.*
import androidx.compose.runtime.*
@ -30,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
@ -50,13 +52,8 @@ fun TipsAndSupport(
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)
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 clickTime by remember { mutableStateOf(System.currentTimeMillis() - 2000) }
var pressAMP by remember { mutableStateOf(16f) }
val animatedPress by animateFloatAsState(
targetValue = pressAMP,
@ -88,7 +85,16 @@ fun TipsAndSupport(
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 = {
@ -110,7 +116,9 @@ fun TipsAndSupport(
pressAMP = 16f
},
onTap = {
context.showToast(checkingUpdates)
if (System.currentTimeMillis() - clickTime > 2000) {
clickTime = System.currentTimeMillis()
context.showToast(context.getString(R.string.checking_updates))
scope.launch {
updateViewModel.dispatch(
UpdateViewAction.CheckUpdate(
@ -121,11 +129,18 @@ fun TipsAndSupport(
)
},
{
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.playSoundEffect(SoundEffectConstants.CLICK)
context.showToast(comingSoon)
})
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)
)
)
context.showToast(context.getString(R.string.coming_soon))
})
Spacer(modifier = Modifier.width(16.dp))
@ -214,19 +214,34 @@ fun TipsAndSupport(
context.startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(githubLink)
Uri.parse(context.getString(R.string.github_link))
)
)
})
Spacer(modifier = Modifier.width(16.dp))
// License
RoundIconButton(RoundIconButtonType.License(
// 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(context.getString(R.string.telegram_link))
)
)
})
Spacer(modifier = Modifier.width(16.dp))
// Help
RoundIconButton(RoundIconButtonType.Help(
backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
context.showToast(comingSoon)
context.showToast(context.getString(R.string.coming_soon))
})
}
Spacer(modifier = Modifier.height(48.dp))
@ -235,10 +250,7 @@ fun TipsAndSupport(
}
)
UpdateDialog(
visible = viewState.updateDialogVisible,
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
)
UpdateDialog()
}
@Immutable
@ -247,6 +259,7 @@ sealed class RoundIconButtonType(
val iconVector: ImageVector? = null,
val descResource: Int? = null,
val descString: String? = null,
open val size: Dp = 24.dp,
open val offset: Modifier = Modifier.offset(),
open val backgroundColor: Color = Color.Unspecified,
open val onClick: () -> Unit = {},
@ -263,6 +276,18 @@ sealed class RoundIconButtonType(
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
data class Telegram(
val desc: String = "Telegram",
@ -277,24 +302,13 @@ sealed class RoundIconButtonType(
)
@Immutable
data class GitHub(
val desc: String = "GitHub",
data class Help(
val desc: Int = R.string.help,
override val offset: Modifier = Modifier.offset(x = (3).dp),
override val backgroundColor: Color,
override val onClick: () -> Unit = {},
) : RoundIconButtonType(
iconResource = R.drawable.ic_github,
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,
iconVector = Icons.Rounded.TipsAndUpdates,
descResource = desc,
backgroundColor = backgroundColor,
onClick = onClick,
@ -313,9 +327,9 @@ private fun RoundIconButton(type: RoundIconButtonType) {
onClick = { type.onClick() }
) {
when (type) {
is RoundIconButtonType.Sponsor, is RoundIconButtonType.License -> {
is RoundIconButtonType.Sponsor, is RoundIconButtonType.Help -> {
Icon(
modifier = type.offset,
modifier = type.offset.size(type.size),
imageVector = type.iconVector!!,
contentDescription = stringResource(type.descResource!!),
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
@ -323,7 +337,7 @@ private fun RoundIconButton(type: RoundIconButtonType) {
}
is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> {
Icon(
modifier = type.offset,
modifier = type.offset.size(type.size),
painter = painterResource(type.iconResource!!),
contentDescription = type.descString,
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,

View File

@ -1,6 +1,13 @@
package me.ash.reader.ui.page.settings
import android.Manifest
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.Spacer
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.data.source.Download
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
import me.ash.reader.ui.ext.*
@SuppressLint("FlowOperatorInvokedInComposition")
@OptIn(ExperimentalPagerApi::class)
@Composable
fun UpdateDialog(
modifier: Modifier = Modifier,
visible: Boolean = false,
onDismissRequest: () -> Unit = {},
onConfirm: (String) -> Unit = {},
updateViewModel: UpdateViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val viewState = updateViewModel.viewState.collectAsStateValue()
val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value
val scope = rememberCoroutineScope { Dispatchers.IO }
val newVersionNumber = context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
@ -55,17 +61,38 @@ fun UpdateDialog(
.map { it[DataStoreKeys.NewVersionLog.key] ?: "" }
.collectAsState(initial = "")
.value
val newVersionSize = context.dataStore.data
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
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(
modifier = modifier.heightIn(max = 400.dp),
visible = visible,
onDismissRequest = onDismissRequest,
modifier = Modifier.heightIn(max = 400.dp),
visible = viewState.updateDialogVisible,
onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
icon = {
Icon(
imageVector = Icons.Rounded.Update,
@ -79,7 +106,7 @@ fun UpdateDialog(
Text(text = stringResource(R.string.change_log))
Spacer(modifier = Modifier.height(16.dp))
Text(
text = newVersionPublishDate,
text = "$newVersionPublishDate$newVersionSize",
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium,
)
@ -97,20 +124,48 @@ fun UpdateDialog(
confirmButton = {
TextButton(
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 = {
TextButton(onClick = {
if (downloadState !is Download.Progress) {
TextButton(
onClick = {
scope.launch {
context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
onDismissRequest()
updateViewModel.dispatch(UpdateViewAction.Hide)
}
}) {
}
) {
Text(text = stringResource(R.string.skip_this_version))
}
}
},
)
}

View File

@ -3,12 +3,10 @@ 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.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.repository.AppRepository
import me.ash.reader.data.source.Download
import javax.inject.Inject
@HiltViewModel
@ -26,6 +24,7 @@ class UpdateViewModel @Inject constructor(
action.preProcessor,
action.postProcessor
)
is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url)
}
}
@ -36,11 +35,13 @@ class UpdateViewModel @Inject constructor(
viewModelScope.launch {
preProcessor()
appRepository.checkUpdate().let {
it?.let {
changeUpdateDialogVisible(it)
postProcessor(it)
}
}
}
}
private fun changeUpdateDialogVisible(visible: Boolean) {
_viewState.update {
@ -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(
val updateDialogVisible: Boolean = false,
val downloadFlow: Flow<Download> = emptyFlow(),
)
sealed class UpdateViewAction {
@ -63,4 +80,8 @@ sealed class UpdateViewAction {
val preProcessor: suspend () -> Unit = {},
val postProcessor: suspend (Boolean) -> Unit = {}
) : UpdateViewAction()
data class DownloadUpdate(
val url: String,
) : UpdateViewAction()
}

View File

@ -117,4 +117,8 @@
<string name="skip_this_version">跳过这个版本</string>
<string name="checking_updates">正在检查更新…</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>

View File

@ -117,4 +117,8 @@
<string name="skip_this_version">Skip This Version</string>
<string name="checking_updates">Checking for updates…</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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="."/>
</paths>