add chat basics
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<resources>
|
||||
<string name="updater_title_available">Доступно обновление на версию %1$s</string>
|
||||
<string name="updater_title_not_found">Обновления не найдены</string>
|
||||
<string name="updater_title_completed">Обновление успешно установлено</string>
|
||||
|
||||
<string name="updater_btn_close">Закрыть</string>
|
||||
|
||||
<string name="updater_btn_download">Скачать</string>
|
||||
<string name="updater_changelog_title">Что нового:</string>
|
||||
<string name="updater_changelog_empty">Улучшения производительности и исправления ошибок.</string>
|
||||
<string name="updater_title_permission">Нужно разрешение</string>
|
||||
<string name="updater_desc_permission">Для установки обновления приложению требуется разрешение на установку из неизвестных источников. Пожалуйста, включите его в настройках.</string>
|
||||
<string name="updater_btn_grant">В настройки</string>
|
||||
<string name="updater_title_downloading">Скачивание обновления…</string>
|
||||
<string name="updater_download_progress">%1$s МБ из %2$s МБ</string>
|
||||
<string name="updater_title_ready">Обновление готово к установке</string>
|
||||
<string name="updater_btn_install">Установить</string>
|
||||
<string name="updater_title_error">Ошибка обновления</string>
|
||||
<string name="updater_btn_retry">Повторить</string>
|
||||
</resources>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package ru.shadowsparky.updater
|
||||
|
||||
import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Configuration
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
@Module
|
||||
@ComponentScan
|
||||
@Configuration
|
||||
class UpdaterModule
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.shadowsparky.updater.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UpdateDownloader {
|
||||
fun download(url: String): Flow<DownloadProgress>
|
||||
|
||||
suspend fun clean()
|
||||
}
|
||||
|
||||
sealed interface DownloadProgress {
|
||||
data class Active(val progress: Float, val downloadedBytes: Long, val totalBytes: Long) : DownloadProgress
|
||||
data class Success(val path: String) : DownloadProgress
|
||||
data class Error(val throwable: Throwable) : DownloadProgress
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ru.shadowsparky.updater.domain
|
||||
|
||||
interface UpdateInstaller {
|
||||
suspend fun hasInstallPermission(): Boolean
|
||||
suspend fun requestInstallPermission()
|
||||
suspend fun install(path: String)
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package ru.shadowsparky.updater.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.domain.AppConfig
|
||||
|
||||
@Factory
|
||||
class UpdateInteractor(
|
||||
private val fetcher: UpdateFetcher?,
|
||||
private val downloader: UpdateDownloader?,
|
||||
private val installer: UpdateInstaller?,
|
||||
private val appConfig: AppConfig?
|
||||
) {
|
||||
val hasSupport = fetcher != null && downloader != null && installer != null &&
|
||||
appConfig?.versionCode != null
|
||||
|
||||
fun checkUpdate(): Flow<UpdateState> = flow {
|
||||
if (appConfig?.versionCode == null || fetcher == null) {
|
||||
emit(UpdateState.UpToDate)
|
||||
return@flow
|
||||
}
|
||||
emit(UpdateState.Checking)
|
||||
try {
|
||||
val currentVersion = appConfig.versionCode ?: 0L
|
||||
val updateInfo = fetcher.fetch(currentVersion)
|
||||
val serverVersion = updateInfo.versionCode ?: 0L
|
||||
|
||||
if (serverVersion <= currentVersion) {
|
||||
emit(UpdateState.UpToDate)
|
||||
} else {
|
||||
emit(
|
||||
UpdateState.UpdateAvailable(
|
||||
url = checkNotNull(updateInfo.url) { "url required" },
|
||||
versionName = updateInfo.versionName ?: "v$serverVersion",
|
||||
changeLog = updateInfo.changelog,
|
||||
isForceUpdate = updateInfo.force
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emit(UpdateState.Error(message = e.message ?: "Ошибка проверки", throwable = e))
|
||||
}
|
||||
}
|
||||
|
||||
fun startDownload(url: String): Flow<UpdateState> = flow {
|
||||
if (installer == null || downloader == null) {
|
||||
emit(UpdateState.UpToDate)
|
||||
return@flow
|
||||
}
|
||||
if (!installer.hasInstallPermission()) {
|
||||
emit(UpdateState.InstallPermissionRequired)
|
||||
return@flow
|
||||
}
|
||||
emit(UpdateState.Downloading(0f, 0, 0))
|
||||
downloader.download(url).firstOrNull { progress ->
|
||||
when (progress) {
|
||||
is DownloadProgress.Active -> {
|
||||
emit(
|
||||
UpdateState.Downloading(
|
||||
progress = progress.progress,
|
||||
downloadedBytes = progress.downloadedBytes,
|
||||
totalBytes = progress.totalBytes
|
||||
)
|
||||
)
|
||||
false
|
||||
}
|
||||
is DownloadProgress.Success -> {
|
||||
emit(UpdateState.ReadyToInstall(filePath = progress.path))
|
||||
true
|
||||
}
|
||||
is DownloadProgress.Error -> {
|
||||
emit(UpdateState.Error(message = progress.throwable.message ?: "Ошибка скачивания", throwable = progress.throwable))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestInstallPermission() {
|
||||
installer?.requestInstallPermission()
|
||||
}
|
||||
|
||||
suspend fun installApk(path: String) {
|
||||
installer?.install(path)
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
downloader?.clean()
|
||||
}
|
||||
|
||||
suspend fun hasInstallPermission(): Boolean {
|
||||
return installer?.hasInstallPermission() ?: false
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package ru.shadowsparky.updater.domain
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface UpdateState {
|
||||
|
||||
data object Idle : UpdateState
|
||||
|
||||
data object Checking : UpdateState
|
||||
|
||||
data class UpdateAvailable(
|
||||
val url: String,
|
||||
val versionName: String,
|
||||
val changeLog: String?,
|
||||
val isForceUpdate: Boolean
|
||||
) : UpdateState
|
||||
|
||||
data object InstallPermissionRequired : UpdateState
|
||||
|
||||
data object UpToDate : UpdateState
|
||||
|
||||
data object Completed : UpdateState
|
||||
|
||||
data class Downloading(
|
||||
val progress: Float,
|
||||
val downloadedBytes: Long,
|
||||
val totalBytes: Long
|
||||
) : UpdateState
|
||||
|
||||
data class ReadyToInstall(val filePath: String) : UpdateState
|
||||
|
||||
data class Error(
|
||||
val message: String,
|
||||
val throwable: Throwable? = null
|
||||
) : UpdateState
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package ru.shadowsparky.updater.presentation
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.value.MutableValue
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
||||
import com.arkivanov.essenty.lifecycle.doOnResume
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.updater.domain.UpdateInteractor
|
||||
import ru.shadowsparky.updater.domain.UpdateState
|
||||
|
||||
class UpdateComponent(
|
||||
componentContext: ComponentContext,
|
||||
private val interactor: UpdateInteractor
|
||||
) : ComponentContext by componentContext {
|
||||
val hasSupport = interactor.hasSupport
|
||||
var isForceUpdate = false
|
||||
private set
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
_state.value = UpdateState.Error(throwable.message ?: "$throwable", throwable)
|
||||
}
|
||||
|
||||
private var lastJob: Job? = null
|
||||
private val scope = CoroutineScope(exceptionHandler + SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
private val _state = MutableValue<UpdateState>(UpdateState.Idle)
|
||||
val state: Value<UpdateState> = _state
|
||||
|
||||
private var pendingUrl: String? = null
|
||||
|
||||
init {
|
||||
lifecycle.doOnDestroy { scope.cancel() }
|
||||
lifecycle.doOnResume {
|
||||
val url = pendingUrl
|
||||
if (_state.value is UpdateState.InstallPermissionRequired && url != null) {
|
||||
scope.launch {
|
||||
if (interactor.hasInstallPermission()) downloadUpdate(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdates(byUser: Boolean = true) {
|
||||
if (!hasSupport) return
|
||||
lastJob = scope.launch {
|
||||
interactor.checkUpdate().collect {
|
||||
isForceUpdate = (it as? UpdateState.UpdateAvailable)?.isForceUpdate ?: false
|
||||
_state.value = if (byUser) {
|
||||
it
|
||||
} else {
|
||||
when (it) {
|
||||
UpdateState.Checking,
|
||||
UpdateState.UpToDate,
|
||||
is UpdateState.Error -> UpdateState.Idle
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadUpdate(url: String) {
|
||||
pendingUrl = url
|
||||
lastJob = scope.launch {
|
||||
interactor.startDownload(url).collect { newState ->
|
||||
_state.value = newState
|
||||
if (newState !is UpdateState.InstallPermissionRequired) {
|
||||
pendingUrl = null
|
||||
}
|
||||
if (newState is UpdateState.ReadyToInstall) {
|
||||
installApk(newState.filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermission() {
|
||||
scope.launch { interactor.requestInstallPermission() }
|
||||
}
|
||||
|
||||
fun installApk(path: String) {
|
||||
scope.launch {
|
||||
_state.value = UpdateState.Checking
|
||||
interactor.installApk(path)
|
||||
_state.value = UpdateState.Completed
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
scope.launch {
|
||||
_state.value = UpdateState.Checking
|
||||
if (lastJob?.isActive == true) lastJob?.cancel()
|
||||
interactor.reset()
|
||||
_state.value = UpdateState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Factory
|
||||
class UpdateComponentFactory(private val interactor: UpdateInteractor) {
|
||||
fun create(componentContext: ComponentContext): UpdateComponent {
|
||||
return UpdateComponent(componentContext, interactor)
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package ru.shadowsparky.updater.presentation
|
||||
|
||||
import androiddev.libs.updater.updater_client.generated.resources.Res
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_close
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_download
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_grant
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_install
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_retry
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_empty
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_title
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_desc_permission
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_download_progress
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_available
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_completed
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_downloading
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_error
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_not_found
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.shadowsparky.ui.components.AlertContent
|
||||
import ru.shadowsparky.ui.components.LazyColumn
|
||||
import ru.shadowsparky.ui.components.Loading
|
||||
import ru.shadowsparky.updater.domain.UpdateState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UpdateDialog(
|
||||
component: UpdateComponent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val state by component.state.subscribeAsState()
|
||||
val canDismiss = remember(state) { !component.isForceUpdate }
|
||||
val isVisible = remember(state) { state !is UpdateState.Idle }
|
||||
if (isVisible) {
|
||||
AlertContent(
|
||||
modifier = modifier,
|
||||
onDismissRequest = { component.reset() },
|
||||
canDismiss = canDismiss,
|
||||
content = {
|
||||
when (val currentState = state) {
|
||||
is UpdateState.Checking -> UpdateCheckingContent()
|
||||
is UpdateState.UpdateAvailable -> UpdateAvailableContent(
|
||||
versionName = currentState.versionName,
|
||||
changeLog = currentState.changeLog,
|
||||
onDownloadClick = { component.downloadUpdate(currentState.url) }
|
||||
)
|
||||
|
||||
is UpdateState.InstallPermissionRequired -> UpdatePermissionContent(
|
||||
onGrantClick = { component.requestPermission() }
|
||||
)
|
||||
|
||||
is UpdateState.Downloading -> UpdateDownloadingContent(
|
||||
progress = currentState.progress,
|
||||
downloadedBytes = currentState.downloadedBytes,
|
||||
totalBytes = currentState.totalBytes
|
||||
)
|
||||
|
||||
is UpdateState.ReadyToInstall -> UpdateReadyContent(
|
||||
onInstallClick = { component.installApk(currentState.filePath) }
|
||||
)
|
||||
|
||||
is UpdateState.Error -> UpdateErrorContent(
|
||||
message = currentState.message,
|
||||
onRetryClick = { component.checkUpdates() }
|
||||
)
|
||||
|
||||
is UpdateState.UpToDate -> {
|
||||
UpdateFinishedContent(onSkip = { component.reset() })
|
||||
}
|
||||
|
||||
is UpdateState.Completed -> {
|
||||
UpdateFinishedContent(
|
||||
Res.string.updater_title_completed,
|
||||
onSkip = { component.reset() }
|
||||
)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateCheckingContent() {
|
||||
Loading(Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateAvailableContent(
|
||||
versionName: String,
|
||||
changeLog: String?,
|
||||
onDownloadClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_title_available, versionName),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_changelog_title),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
item {
|
||||
Button(onClick = onDownloadClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_download),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdatePermissionContent(
|
||||
onGrantClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_title_permission),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_desc_permission),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Button(onClick = onGrantClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_grant),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateDownloadingContent(
|
||||
progress: Float,
|
||||
downloadedBytes: Long,
|
||||
totalBytes: Long,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_title_downloading),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
|
||||
|
||||
val downloadedMb = ((downloadedBytes / (1024f * 1024f)) * 10).toInt() / 10.0
|
||||
val totalMb = ((totalBytes / (1024f * 1024f)) * 10).toInt() / 10.0
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
Res.string.updater_download_progress,
|
||||
downloadedMb.toString(),
|
||||
totalMb.toString()
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateReadyContent(
|
||||
onInstallClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_title_ready),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Button(onClick = onInstallClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_install),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateFinishedContent(
|
||||
text: StringResource = Res.string.updater_title_not_found,
|
||||
onSkip: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(text = stringResource(text), style = MaterialTheme.typography.titleLarge)
|
||||
Button(onClick = onSkip, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_close),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateErrorContent(
|
||||
message: String,
|
||||
onRetryClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_title_error),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(text = message, style = MaterialTheme.typography.bodyMedium)
|
||||
Button(onClick = onRetryClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_retry),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UpdateAvailablePreview() {
|
||||
MaterialTheme {
|
||||
UpdateAvailableContent(
|
||||
versionName = "v1.4.0",
|
||||
changeLog = "- Добавили темную тему\n- Исправили падение на главном экране\n- Повысили стабильность",
|
||||
onDownloadClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UpdateDownloadingPreview() {
|
||||
MaterialTheme {
|
||||
UpdateDownloadingContent(
|
||||
progress = 0.45f,
|
||||
downloadedBytes = 24_117_248L, // ~23 MB
|
||||
totalBytes = 52_428_800L // 50 MB
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UpdatePermissionPreview() {
|
||||
MaterialTheme {
|
||||
UpdatePermissionContent(onGrantClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UpdateErrorPreview() {
|
||||
MaterialTheme {
|
||||
UpdateErrorContent(message = "Http status 503: Service Unavailable", onRetryClick = {})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user