mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: update banner layout
This commit is contained in:
parent
409a2e9b01
commit
cc59387588
12 changed files with 544 additions and 283 deletions
|
|
@ -2,6 +2,7 @@ package com.nuvio.app.features.updater
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
|
|
@ -40,6 +41,11 @@ object AndroidAppUpdaterPlatform {
|
|||
|
||||
fun getSupportedAbis(): List<String> = Build.SUPPORTED_ABIS?.toList().orEmpty()
|
||||
|
||||
fun isDebugBuild(): Boolean {
|
||||
val context = appContext ?: return false
|
||||
return context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
}
|
||||
|
||||
fun getIgnoredTag(): String? =
|
||||
preferences().getString(ignoredTagKey, null)
|
||||
|
||||
|
|
@ -141,4 +147,4 @@ object AndroidAppUpdaterPlatform {
|
|||
|
||||
private fun requireContext(): Context =
|
||||
requireNotNull(appContext) { "AndroidAppUpdaterPlatform.initialize must be called before use." }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.nuvio.app.features.updater
|
|||
|
||||
actual object AppUpdaterPlatform {
|
||||
actual val isSupported: Boolean = true
|
||||
actual val isDebugBuild: Boolean
|
||||
get() = AndroidAppUpdaterPlatform.isDebugBuild()
|
||||
|
||||
actual fun getSupportedAbis(): List<String> = AndroidAppUpdaterPlatform.getSupportedAbis()
|
||||
|
||||
|
|
@ -24,4 +26,4 @@ actual object AppUpdaterPlatform {
|
|||
}
|
||||
|
||||
actual fun installDownloadedApk(path: String): Result<Unit> = AndroidAppUpdaterPlatform.installDownloadedApk(path)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.jetbrains.compose.resources.getString
|
|||
|
||||
actual object AppUpdaterPlatform {
|
||||
actual val isSupported: Boolean = false
|
||||
actual val isDebugBuild: Boolean = false
|
||||
|
||||
actual fun getSupportedAbis(): List<String> = emptyList()
|
||||
|
||||
|
|
@ -26,4 +27,4 @@ actual object AppUpdaterPlatform {
|
|||
|
||||
actual fun installDownloadedApk(path: String): Result<Unit> =
|
||||
Result.failure(IllegalStateException(runBlocking { getString(Res.string.updates_not_available) }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1406,6 +1406,9 @@
|
|||
<string name="updates_download_empty_body">Empty download body</string>
|
||||
<string name="updates_download_file_missing">Downloaded update file is missing.</string>
|
||||
<string name="updates_downloading_progress">Downloading %1$d%</string>
|
||||
<string name="updates_debug_test_complete">Banner test complete</string>
|
||||
<string name="updates_debug_test_description">Preview the banner and simulated download progress.</string>
|
||||
<string name="updates_debug_test_title">Test update banner</string>
|
||||
<string name="updates_install_failed">Unable to start installation</string>
|
||||
<string name="updates_latest_version">You're using the latest version.</string>
|
||||
<string name="updates_message_allow_installs">Enable app installs for Nuvio, then come back and continue.</string>
|
||||
|
|
@ -1413,6 +1416,7 @@
|
|||
<string name="updates_message_no_updates">No updates found.</string>
|
||||
<string name="updates_message_ready">A new version is ready to install.</string>
|
||||
<string name="updates_not_available">In-app updates are not available on this build.</string>
|
||||
<string name="updates_no_release_notes">No release notes were provided.</string>
|
||||
<string name="updates_preparing_download">Preparing download</string>
|
||||
<string name="updates_release_notes">Release notes</string>
|
||||
<string name="updates_title_allow_installs">Allow installs to continue</string>
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ import com.nuvio.app.features.trakt.TraktAuthRepository
|
|||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.trakt.TraktScrobbleRepository
|
||||
import com.nuvio.app.features.updater.AppUpdaterHost
|
||||
import com.nuvio.app.features.updater.AppUpdaterPlatform
|
||||
import com.nuvio.app.features.updater.rememberAppUpdaterController
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
|
|
@ -1781,11 +1782,15 @@ private fun MainAppContent(
|
|||
selectedContinueWatchingForActions = item
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.nuvio.colors.background),
|
||||
AppUpdaterHost(
|
||||
controller = appUpdaterController,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.nuvio.colors.background),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -1989,6 +1994,13 @@ private fun MainAppContent(
|
|||
} else {
|
||||
null
|
||||
},
|
||||
onTestUpdateBannerClick = if (
|
||||
AppFeaturePolicy.inAppUpdaterEnabled && AppUpdaterPlatform.isDebugBuild
|
||||
) {
|
||||
appUpdaterController::showDebugTestUpdate
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onCollectionsSettingsClick = { navController.navigate(CollectionsRoute(collectionsTitle)) },
|
||||
onFolderClick = { collectionId, folderId ->
|
||||
val folderTitle = CollectionRepository.collections.value
|
||||
|
|
@ -3003,6 +3015,13 @@ private fun MainAppContent(
|
|||
} else {
|
||||
null
|
||||
},
|
||||
onTestUpdateBannerClick = if (
|
||||
AppFeaturePolicy.inAppUpdaterEnabled && AppUpdaterPlatform.isDebugBuild
|
||||
) {
|
||||
appUpdaterController::showDebugTestUpdate
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
entry<DownloadsSettingsRoute> { route ->
|
||||
|
|
@ -3480,12 +3499,7 @@ private fun MainAppContent(
|
|||
.zIndex(20f),
|
||||
)
|
||||
|
||||
AppUpdaterHost(
|
||||
controller = appUpdaterController,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.zIndex(25f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3541,6 +3555,7 @@ private fun AppTabHost(
|
|||
onSupportersContributorsSettingsClick: () -> Unit = {},
|
||||
onLicensesAttributionsSettingsClick: () -> Unit = {},
|
||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onTestUpdateBannerClick: (() -> Unit)? = null,
|
||||
onCollectionsSettingsClick: () -> Unit = {},
|
||||
onFolderClick: ((collectionId: String, folderId: String) -> Unit)? = null,
|
||||
requestedSettingsPageName: String? = null,
|
||||
|
|
@ -3608,6 +3623,7 @@ private fun AppTabHost(
|
|||
onSupportersContributorsClick = onSupportersContributorsSettingsClick,
|
||||
onLicensesAttributionsClick = onLicensesAttributionsSettingsClick,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onTestUpdateBannerClick = onTestUpdateBannerClick,
|
||||
onCollectionsClick = onCollectionsSettingsClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.AccountCircle
|
||||
import androidx.compose.material.icons.rounded.BugReport
|
||||
import androidx.compose.material.icons.rounded.CloudDownload
|
||||
import androidx.compose.material.icons.rounded.Extension
|
||||
import androidx.compose.material.icons.rounded.Favorite
|
||||
|
|
@ -52,6 +53,8 @@ import nuvio.composeapp.generated.resources.compose_settings_root_advanced_secti
|
|||
import nuvio.composeapp.generated.resources.compose_settings_page_content_discovery
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_trakt
|
||||
import nuvio.composeapp.generated.resources.settings_playback_subtitle
|
||||
import nuvio.composeapp.generated.resources.updates_debug_test_description
|
||||
import nuvio.composeapp.generated.resources.updates_debug_test_title
|
||||
import nuvio.composeapp.generated.resources.about_supporters_contributors_subtitle
|
||||
import nuvio.composeapp.generated.resources.about_licenses_attributions_subtitle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -68,6 +71,7 @@ internal fun LazyListScope.settingsRootContent(
|
|||
onSupportersContributorsClick: () -> Unit,
|
||||
onLicensesAttributionsClick: () -> Unit,
|
||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onTestUpdateBannerClick: (() -> Unit)? = null,
|
||||
onDownloadsClick: () -> Unit,
|
||||
onAccountClick: () -> Unit,
|
||||
onSwitchProfileClick: (() -> Unit)? = null,
|
||||
|
|
@ -205,6 +209,16 @@ internal fun LazyListScope.settingsRootContent(
|
|||
onClick = onCheckForUpdatesClick,
|
||||
)
|
||||
}
|
||||
if (onTestUpdateBannerClick != null) {
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = stringResource(Res.string.updates_debug_test_title),
|
||||
description = stringResource(Res.string.updates_debug_test_description),
|
||||
icon = Icons.Rounded.BugReport,
|
||||
isTablet = isTablet,
|
||||
onClick = onTestUpdateBannerClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ fun SettingsScreen(
|
|||
onSupportersContributorsClick: () -> Unit = {},
|
||||
onLicensesAttributionsClick: () -> Unit = {},
|
||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onTestUpdateBannerClick: (() -> Unit)? = null,
|
||||
onCollectionsClick: () -> Unit = {},
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
|
|
@ -410,6 +411,7 @@ fun SettingsScreen(
|
|||
onSupportersContributorsClick = openSupportersContributors,
|
||||
onLicensesAttributionsClick = openLicensesAttributions,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onTestUpdateBannerClick = onTestUpdateBannerClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -473,6 +475,7 @@ fun SettingsScreen(
|
|||
onSupportersContributorsClick = openSupportersContributors,
|
||||
onLicensesAttributionsClick = openLicensesAttributions,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onTestUpdateBannerClick = onTestUpdateBannerClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -540,6 +543,7 @@ private fun MobileSettingsScreen(
|
|||
onSupportersContributorsClick: () -> Unit = {},
|
||||
onLicensesAttributionsClick: () -> Unit = {},
|
||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onTestUpdateBannerClick: (() -> Unit)? = null,
|
||||
onCollectionsClick: () -> Unit = {},
|
||||
) {
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
|
@ -654,6 +658,7 @@ private fun MobileSettingsScreen(
|
|||
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||
onLicensesAttributionsClick = onLicensesAttributionsClick,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onTestUpdateBannerClick = onTestUpdateBannerClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = onAccountClick,
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
|
|
@ -884,6 +889,7 @@ private fun TabletSettingsScreen(
|
|||
onSupportersContributorsClick: () -> Unit = {},
|
||||
onLicensesAttributionsClick: () -> Unit = {},
|
||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onTestUpdateBannerClick: (() -> Unit)? = null,
|
||||
onCollectionsClick: () -> Unit = {},
|
||||
) {
|
||||
var selectedCategory by rememberSaveable { mutableStateOf(SettingsCategory.General.name) }
|
||||
|
|
@ -1056,6 +1062,7 @@ private fun TabletSettingsScreen(
|
|||
onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
|
||||
onLicensesAttributionsClick = { openInlinePage(SettingsPage.LicensesAttributions) },
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onTestUpdateBannerClick = onTestUpdateBannerClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = { openInlinePage(SettingsPage.Account) },
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
|
|
|
|||
|
|
@ -1,45 +1,15 @@
|
|||
package com.nuvio.app.features.updater
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import com.nuvio.app.core.ui.NuvioLoadingIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.build.AppVersionConfig
|
||||
import com.nuvio.app.core.i18n.localizedByteUnit
|
||||
import com.nuvio.app.core.ui.NuvioToastController
|
||||
import com.nuvio.app.features.addons.httpRequestRaw
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -52,7 +22,6 @@ import kotlinx.serialization.json.Json
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
private const val gitHubOwner = "NuvioMedia"
|
||||
private const val gitHubRepo = "NuvioMobile"
|
||||
|
|
@ -79,6 +48,7 @@ data class AppUpdaterUiState(
|
|||
val showDialog: Boolean = false,
|
||||
val showUnknownSourcesDialog: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val isDebugTest: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -250,6 +220,7 @@ class AppUpdaterController internal constructor(
|
|||
isChecking = true,
|
||||
errorMessage = null,
|
||||
showUnknownSourcesDialog = false,
|
||||
isDebugTest = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -322,6 +293,10 @@ class AppUpdaterController internal constructor(
|
|||
|
||||
fun downloadUpdate() {
|
||||
val update = _uiState.value.update ?: return
|
||||
if (_uiState.value.isDebugTest) {
|
||||
runDebugDownloadTest()
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
_uiState.update { state ->
|
||||
|
|
@ -346,7 +321,7 @@ class AppUpdaterController internal constructor(
|
|||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isDownloading = false,
|
||||
downloadProgress = 1f,
|
||||
downloadProgress = null,
|
||||
downloadedApkPath = path,
|
||||
errorMessage = null,
|
||||
)
|
||||
|
|
@ -395,6 +370,56 @@ class AppUpdaterController internal constructor(
|
|||
AppUpdaterPlatform.openUnknownSourcesSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun showDebugTestUpdate() {
|
||||
if (!AppUpdaterPlatform.isDebugBuild || !AppUpdaterPlatform.isSupported) return
|
||||
|
||||
_uiState.value = AppUpdaterUiState(
|
||||
update = AppUpdate(
|
||||
tag = "9.9.9",
|
||||
title = "Nuvio 9.9.9",
|
||||
notes = """
|
||||
A local preview of the new update experience.
|
||||
|
||||
- The banner pushes the app content down.
|
||||
- Download progress fills the banner with the primary accent.
|
||||
- Release notes live behind the info button.
|
||||
""".trimIndent(),
|
||||
releaseUrl = null,
|
||||
assetName = "Nuvio-debug-preview.apk",
|
||||
assetUrl = "debug://update-preview",
|
||||
assetSizeBytes = 185L * 1024L * 1024L,
|
||||
),
|
||||
isUpdateAvailable = true,
|
||||
showDialog = true,
|
||||
isDebugTest = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun runDebugDownloadTest() {
|
||||
scope.launch {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isDownloading = true,
|
||||
downloadProgress = 0f,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
|
||||
for (step in 1..100) {
|
||||
delay(35)
|
||||
_uiState.update { state -> state.copy(downloadProgress = step / 100f) }
|
||||
}
|
||||
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isDownloading = false,
|
||||
isUpdateAvailable = false,
|
||||
downloadProgress = 1f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -403,240 +428,7 @@ fun rememberAppUpdaterController(): AppUpdaterController {
|
|||
return remember(scope) { AppUpdaterController(scope) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppUpdaterHost(
|
||||
controller: AppUpdaterController,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (!AppFeaturePolicy.inAppUpdaterEnabled || !AppUpdaterPlatform.isSupported) {
|
||||
return
|
||||
}
|
||||
|
||||
val state by controller.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
controller.ensureAutoCheckStarted()
|
||||
}
|
||||
|
||||
if (!state.showDialog) return
|
||||
|
||||
val showPrimaryAction =
|
||||
state.showUnknownSourcesDialog || state.isDownloading || state.downloadedApkPath != null || state.isUpdateAvailable
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = {
|
||||
if (!state.isDownloading) {
|
||||
controller.dismissDialog()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp,
|
||||
shadowElevation = 16.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = when {
|
||||
state.showUnknownSourcesDialog -> stringResource(Res.string.updates_title_allow_installs)
|
||||
state.isUpdateAvailable -> state.update?.title ?: stringResource(Res.string.updates_title_available)
|
||||
else -> stringResource(Res.string.updates_title_status)
|
||||
},
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = when {
|
||||
state.showUnknownSourcesDialog -> stringResource(Res.string.updates_message_allow_installs)
|
||||
state.isDownloading -> stringResource(Res.string.updates_message_downloading)
|
||||
state.isUpdateAvailable -> stringResource(Res.string.updates_message_ready)
|
||||
else -> stringResource(Res.string.updates_message_no_updates)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
state.errorMessage?.let { message ->
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
state.update?.let { update ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (state.isChecking) {
|
||||
NuvioLoadingIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = update.tag,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
val assetLine = update.assetSizeBytes?.let(::formatFileSize)?.let { size ->
|
||||
stringResource(Res.string.updates_asset_line, size, update.assetName)
|
||||
} ?: update.assetName
|
||||
Text(
|
||||
text = assetLine,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isDownloading || state.downloadProgress != null) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
LinearProgressIndicator(
|
||||
progress = { (state.downloadProgress ?: 0f).coerceIn(0f, 1f) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = if (state.downloadProgress != null) {
|
||||
stringResource(
|
||||
Res.string.updates_downloading_progress,
|
||||
((state.downloadProgress ?: 0f) * 100).toInt().coerceIn(0, 100),
|
||||
)
|
||||
} else {
|
||||
stringResource(Res.string.updates_preparing_download)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (update.notes.isNotBlank()) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updates_release_notes),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = update.notes,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(180.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.padding(14.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (showPrimaryAction) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
when {
|
||||
state.showUnknownSourcesDialog -> controller.resumeInstallation()
|
||||
state.downloadedApkPath != null -> controller.installDownloadedUpdate()
|
||||
else -> controller.downloadUpdate()
|
||||
}
|
||||
},
|
||||
enabled = if (state.showUnknownSourcesDialog || state.downloadedApkPath != null) {
|
||||
true
|
||||
} else {
|
||||
!state.isChecking && !state.isDownloading && state.isUpdateAvailable
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
when {
|
||||
state.showUnknownSourcesDialog -> stringResource(Res.string.action_continue)
|
||||
state.downloadedApkPath != null -> stringResource(Res.string.action_install)
|
||||
state.isDownloading -> stringResource(Res.string.updates_message_downloading)
|
||||
else -> stringResource(Res.string.action_update)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isUpdateAvailable && !state.isDownloading && !state.showUnknownSourcesDialog) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = controller::ignoreThisVersion,
|
||||
) {
|
||||
Text(stringResource(Res.string.action_ignore))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = controller::dismissDialog,
|
||||
enabled = !state.isDownloading,
|
||||
) {
|
||||
Text(
|
||||
if (state.isDownloading) {
|
||||
stringResource(Res.string.updates_message_downloading)
|
||||
} else {
|
||||
stringResource(Res.string.action_later)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = controller::dismissDialog,
|
||||
enabled = !state.isDownloading,
|
||||
) {
|
||||
Text(
|
||||
if (state.isDownloading) {
|
||||
stringResource(Res.string.updates_message_downloading)
|
||||
} else {
|
||||
stringResource(Res.string.action_later)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFileSize(sizeBytes: Long): String {
|
||||
internal fun formatFileSize(sizeBytes: Long): String {
|
||||
if (sizeBytes <= 0L) return "0 ${localizedByteUnit("B")}"
|
||||
val units = listOf("B", "KB", "MB", "GB")
|
||||
var value = sizeBytes.toDouble()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,416 @@
|
|||
package com.nuvio.app.features.updater
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.CheckCircle
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.CloudDownload
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_close
|
||||
import nuvio.composeapp.generated.resources.action_continue
|
||||
import nuvio.composeapp.generated.resources.action_install
|
||||
import nuvio.composeapp.generated.resources.action_later
|
||||
import nuvio.composeapp.generated.resources.action_retry
|
||||
import nuvio.composeapp.generated.resources.action_update
|
||||
import nuvio.composeapp.generated.resources.updates_debug_test_complete
|
||||
import nuvio.composeapp.generated.resources.updates_downloading_progress
|
||||
import nuvio.composeapp.generated.resources.updates_message_allow_installs
|
||||
import nuvio.composeapp.generated.resources.updates_message_ready
|
||||
import nuvio.composeapp.generated.resources.updates_no_release_notes
|
||||
import nuvio.composeapp.generated.resources.updates_preparing_download
|
||||
import nuvio.composeapp.generated.resources.updates_release_notes
|
||||
import nuvio.composeapp.generated.resources.updates_title_allow_installs
|
||||
import nuvio.composeapp.generated.resources.updates_title_available
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun AppUpdaterHost(
|
||||
controller: AppUpdaterController,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
if (!AppFeaturePolicy.inAppUpdaterEnabled || !AppUpdaterPlatform.isSupported) {
|
||||
content()
|
||||
return
|
||||
}
|
||||
|
||||
val state by controller.uiState.collectAsStateWithLifecycle()
|
||||
var showReleaseNotes by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
controller.ensureAutoCheckStarted()
|
||||
}
|
||||
LaunchedEffect(state.update?.tag) {
|
||||
showReleaseNotes = false
|
||||
}
|
||||
|
||||
val update = state.update
|
||||
val showBanner = state.showDialog && update != null
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = showBanner,
|
||||
enter = expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
) + fadeIn(animationSpec = tween(durationMillis = 180)),
|
||||
exit = shrinkVertically(
|
||||
shrinkTowards = Alignment.Top,
|
||||
animationSpec = tween(durationMillis = 240),
|
||||
) + fadeOut(animationSpec = tween(durationMillis = 150)),
|
||||
) {
|
||||
update?.let { availableUpdate ->
|
||||
AppUpdateBanner(
|
||||
state = state,
|
||||
update = availableUpdate,
|
||||
onDownload = controller::downloadUpdate,
|
||||
onInstall = controller::installDownloadedUpdate,
|
||||
onShowReleaseNotes = { showReleaseNotes = true },
|
||||
onDismiss = controller::dismissDialog,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (showBanner) {
|
||||
Modifier.consumeWindowInsets(WindowInsets.statusBars)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
if (showReleaseNotes && update != null) {
|
||||
ReleaseNotesDialog(
|
||||
update = update,
|
||||
onDismiss = { showReleaseNotes = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showUnknownSourcesDialog) {
|
||||
UnknownSourcesDialog(
|
||||
onContinue = controller::resumeInstallation,
|
||||
onDismiss = controller::dismissDialog,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppUpdateBanner(
|
||||
state: AppUpdaterUiState,
|
||||
update: AppUpdate,
|
||||
onDownload: () -> Unit,
|
||||
onInstall: () -> Unit,
|
||||
onShowReleaseNotes: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val targetProgress = when {
|
||||
state.isDownloading -> state.downloadProgress ?: 0f
|
||||
state.isDebugTest && !state.isUpdateAvailable -> 1f
|
||||
else -> 0f
|
||||
}.coerceIn(0f, 1f)
|
||||
val progress by animateFloatAsState(
|
||||
targetValue = targetProgress,
|
||||
animationSpec = tween(durationMillis = 180),
|
||||
label = "updateBannerProgress",
|
||||
)
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
val progressColor = MaterialTheme.colorScheme.primary
|
||||
val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
val debugTestComplete = state.isDebugTest && !state.isDownloading && !state.isUpdateAvailable
|
||||
val subtitle = when {
|
||||
state.errorMessage != null -> state.errorMessage
|
||||
state.isDownloading && state.downloadProgress != null -> stringResource(
|
||||
Res.string.updates_downloading_progress,
|
||||
(state.downloadProgress * 100).toInt().coerceIn(0, 100),
|
||||
)
|
||||
state.isDownloading -> stringResource(Res.string.updates_preparing_download)
|
||||
debugTestComplete -> stringResource(Res.string.updates_debug_test_complete)
|
||||
state.downloadedApkPath != null -> stringResource(Res.string.updates_message_ready)
|
||||
else -> stringResource(Res.string.updates_title_available)
|
||||
}
|
||||
val updateLabel = listOfNotNull(
|
||||
update.tag,
|
||||
update.assetSizeBytes?.let(::formatFileSize),
|
||||
).joinToString(separator = " • ")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
drawRect(containerColor)
|
||||
if (progress > 0f) {
|
||||
drawRect(
|
||||
color = progressColor,
|
||||
size = Size(width = size.width * progress, height = size.height),
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
color = dividerColor,
|
||||
topLeft = Offset(0f, size.height - 1.dp.toPx()),
|
||||
size = Size(width = size.width, height = 1.dp.toPx()),
|
||||
)
|
||||
}
|
||||
.windowInsetsPadding(WindowInsets.statusBars),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 68.dp)
|
||||
.padding(horizontal = tokens.spacing.screenHorizontal, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (debugTestComplete) Icons.Rounded.CheckCircle else Icons.Rounded.CloudDownload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = updateLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = when {
|
||||
state.errorMessage != null -> MaterialTheme.colorScheme.error
|
||||
state.isDownloading -> MaterialTheme.colorScheme.onPrimary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onShowReleaseNotes,
|
||||
enabled = update.notes.isNotBlank(),
|
||||
modifier = Modifier.size(40.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Info,
|
||||
contentDescription = stringResource(Res.string.updates_release_notes),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.isDownloading && !debugTestComplete) {
|
||||
Button(
|
||||
onClick = if (state.downloadedApkPath != null) onInstall else onDownload,
|
||||
enabled = state.downloadedApkPath != null || state.isUpdateAvailable,
|
||||
modifier = Modifier.heightIn(min = 40.dp),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
if (state.downloadedApkPath != null) {
|
||||
stringResource(Res.string.action_install)
|
||||
} else if (state.errorMessage != null) {
|
||||
stringResource(Res.string.action_retry)
|
||||
} else {
|
||||
stringResource(Res.string.action_update)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isDownloading) {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.size(40.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = stringResource(Res.string.action_close),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ReleaseNotesDialog(
|
||||
update: AppUpdate,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 560.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp,
|
||||
shadowElevation = 16.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updates_release_notes),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = update.title,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = stringResource(Res.string.action_close),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
|
||||
Text(
|
||||
text = update.notes.ifBlank { stringResource(Res.string.updates_no_release_notes) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 420.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun UnknownSourcesDialog(
|
||||
onContinue: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 480.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp,
|
||||
shadowElevation = 16.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.updates_title_allow_installs),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.updates_message_allow_installs),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.action_later))
|
||||
}
|
||||
Button(onClick = onContinue) {
|
||||
Text(stringResource(Res.string.action_continue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.nuvio.app.features.updater
|
|||
|
||||
expect object AppUpdaterPlatform {
|
||||
val isSupported: Boolean
|
||||
val isDebugBuild: Boolean
|
||||
|
||||
fun getSupportedAbis(): List<String>
|
||||
|
||||
|
|
@ -20,4 +21,4 @@ expect object AppUpdaterPlatform {
|
|||
fun openUnknownSourcesSettings()
|
||||
|
||||
fun installDownloadedApk(path: String): Result<Unit>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.jetbrains.compose.resources.getString
|
|||
|
||||
actual object AppUpdaterPlatform {
|
||||
actual val isSupported: Boolean = false
|
||||
actual val isDebugBuild: Boolean = false
|
||||
|
||||
actual fun getSupportedAbis(): List<String> = emptyList()
|
||||
|
||||
|
|
@ -26,4 +27,4 @@ actual object AppUpdaterPlatform {
|
|||
|
||||
actual fun installDownloadedApk(path: String): Result<Unit> =
|
||||
Result.failure(IllegalStateException(runBlocking { getString(Res.string.updates_not_available) }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.jetbrains.compose.resources.getString
|
|||
|
||||
actual object AppUpdaterPlatform {
|
||||
actual val isSupported: Boolean = false
|
||||
actual val isDebugBuild: Boolean = false
|
||||
|
||||
actual fun getSupportedAbis(): List<String> = emptyList()
|
||||
|
||||
|
|
@ -26,4 +27,4 @@ actual object AppUpdaterPlatform {
|
|||
|
||||
actual fun installDownloadedApk(path: String): Result<Unit> =
|
||||
Result.failure(IllegalStateException(runBlocking { getString(Res.string.updates_not_available) }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue