From 57ad0402c5dd5b891bb09a1bbda58d5fb4149e16 Mon Sep 17 00:00:00 2001 From: Chris Keenan <10093880+chRyNaN@users.noreply.github.com> Date: Sun, 16 Feb 2025 23:02:19 -0500 Subject: [PATCH] Added App Details bottom sheet to Settings Screen --- .../composeResources/values/strings.xml | 16 +- .../shared/feature/settings/SettingsScreen.kt | 14 +- .../feature/settings/SettingsStateModel.kt | 3 +- .../feature/settings/SettingsViewModel.kt | 18 +- .../composable/AppDetailsBottomSheetLayout.kt | 166 ++++++++++++++++++ .../settings/composable/SettingsAppGroup.kt | 7 +- .../composable/SettingsBottomSheet.kt | 5 + .../settings/model/SettingsAppDetails.kt | 17 ++ .../model/SettingsBottomSheetDestination.kt | 8 + .../vpn/app/shared/info/AppClientInfo.kt | 4 +- 10 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/AppDetailsBottomSheetLayout.kt create mode 100644 app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsAppDetails.kt diff --git a/app-shared/src/commonMain/composeResources/values/strings.xml b/app-shared/src/commonMain/composeResources/values/strings.xml index bc0abcb5..849f1c15 100644 --- a/app-shared/src/commonMain/composeResources/values/strings.xml +++ b/app-shared/src/commonMain/composeResources/values/strings.xml @@ -1,5 +1,10 @@ + N/A + Unexpected error + Yes + No + mooncloak VPN Go dark, stay bright @@ -100,9 +105,6 @@ Gbps Tbps - N/A - Unexpected error - Unexpected error loading VPN servers There was an unexpected error attempting to load the countries that have VPN servers. Please try again later. If the error persists, contact support. Regions @@ -137,6 +139,14 @@ 30 minutes Custom time + App Details + ID + Name + Version + Debug + Pre-release + Build Time + No active plan No Active Plan Active Plan diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsScreen.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsScreen.kt index b2537ae3..beb12f5c 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsScreen.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import com.mooncloak.kodetools.statex.persistence.ExperimentalPersistentStateAPI import com.mooncloak.kodetools.statex.update @@ -71,7 +70,6 @@ public fun SettingsScreen( val bottomSheetState = rememberModalNavigationBottomSheetState() val scrollState = rememberScrollState() val coroutineScope = rememberCoroutineScope() - val uriHandler = LocalUriHandler.current val preferencesStorage = rememberDependency { keyValueStorage.preferences } LaunchedEffect(Unit) { @@ -135,9 +133,19 @@ public fun SettingsScreen( color = MaterialTheme.colorScheme.outline.copy(alpha = SecondaryAlpha) ) + println("appDetails: ${viewModel.state.current.value.appDetails}") + SettingsAppGroup( - appVersion = viewModel.state.current.value.appVersion, + appVersion = viewModel.state.current.value.appDetails?.version, sourceCodeUri = viewModel.state.current.value.sourceCodeUri, + appDetailsEnabled = viewModel.state.current.value.appDetails != null, + onOpenAppDetails = { + viewModel.state.current.value.appDetails?.let { details -> + coroutineScope.launch { + bottomSheetState.show(SettingsBottomSheetDestination.AppInfo(details)) + } + } + }, onOpenDependencyList = { coroutineScope.launch { bottomSheetState.show(SettingsBottomSheetDestination.DependencyLicenseList) diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsStateModel.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsStateModel.kt index a6020a6b..4f360b55 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsStateModel.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsStateModel.kt @@ -1,12 +1,13 @@ package com.mooncloak.vpn.app.shared.feature.settings import androidx.compose.runtime.Immutable +import com.mooncloak.vpn.app.shared.feature.settings.model.SettingsAppDetails import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes @Immutable public data class SettingsStateModel public constructor( - public val appVersion: String? = null, + public val appDetails: SettingsAppDetails? = null, public val currentPlan: String? = null, public val privacyPolicyUri: String? = null, public val termsUri: String? = null, diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsViewModel.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsViewModel.kt index ad3d3bd4..5577f623 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsViewModel.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/SettingsViewModel.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable import com.mooncloak.kodetools.konstruct.annotations.Inject import com.mooncloak.kodetools.logpile.core.LogPile import com.mooncloak.kodetools.logpile.core.error +import com.mooncloak.kodetools.logpile.core.info import com.mooncloak.kodetools.statex.ViewModel import com.mooncloak.kodetools.statex.persistence.ExperimentalPersistentStateAPI import com.mooncloak.kodetools.statex.update import com.mooncloak.vpn.app.shared.di.FeatureScoped +import com.mooncloak.vpn.app.shared.feature.settings.model.SettingsAppDetails import com.mooncloak.vpn.app.shared.info.AppClientInfo import com.mooncloak.vpn.app.shared.resource.Res import com.mooncloak.vpn.app.shared.resource.app_copyright @@ -36,10 +38,11 @@ public class SettingsViewModel @Inject public constructor( @OptIn(ExperimentalPersistentStateAPI::class) public fun load() { + LogPile.info("SettingsViewModel: load") coroutineScope.launch { emit(value = state.current.value.copy(isLoading = true)) - var appVersion: String? = state.current.value.appVersion + var appDetails: SettingsAppDetails? = state.current.value.appDetails var privacyPolicyUri: String? = state.current.value.privacyPolicyUri var termsUri: String? = state.current.value.termsUri var sourceCodeUri: String? = state.current.value.sourceCodeUri @@ -51,7 +54,14 @@ public class SettingsViewModel @Inject public constructor( var systemAuthTimeout = state.current.value.systemAuthTimeout try { - appVersion = appClientInfo.versionName + appDetails = SettingsAppDetails( + id = appClientInfo.id, + name = appClientInfo.name, + version = appClientInfo.versionName, + isDebug = appClientInfo.isDebug, + isPreRelease = appClientInfo.isPreRelease, + buildTime = appClientInfo.buildTime + ) privacyPolicyUri = appClientInfo.privacyPolicyUri termsUri = appClientInfo.termsAndConditionsUri sourceCodeUri = appClientInfo.sourceCodeUri @@ -75,7 +85,7 @@ public class SettingsViewModel @Inject public constructor( emit( value = state.current.value.copy( isLoading = false, - appVersion = appVersion, + appDetails = appDetails, currentPlan = currentPlan, privacyPolicyUri = privacyPolicyUri, termsUri = termsUri, @@ -92,7 +102,7 @@ public class SettingsViewModel @Inject public constructor( value = state.current.value.copy( isLoading = false, errorMessage = e.message ?: getString(Res.string.global_unexpected_error), - appVersion = appVersion, + appDetails = appDetails, currentPlan = currentPlan, privacyPolicyUri = privacyPolicyUri, termsUri = termsUri, diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/AppDetailsBottomSheetLayout.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/AppDetailsBottomSheetLayout.kt new file mode 100644 index 00000000..566d3df8 --- /dev/null +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/AppDetailsBottomSheetLayout.kt @@ -0,0 +1,166 @@ +package com.mooncloak.vpn.app.shared.feature.settings.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mooncloak.vpn.app.shared.feature.settings.model.SettingsAppDetails +import com.mooncloak.vpn.app.shared.resource.Res +import com.mooncloak.vpn.app.shared.resource.global_not_available +import com.mooncloak.vpn.app.shared.resource.global_yes +import com.mooncloak.vpn.app.shared.resource.settings_app_details_header +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_build_time +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_debug +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_id +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_name +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_pre_release +import com.mooncloak.vpn.app.shared.resource.settings_app_details_title_version +import com.mooncloak.vpn.app.shared.theme.SecondaryAlpha +import com.mooncloak.vpn.app.shared.util.time.DateTimeFormatter +import com.mooncloak.vpn.app.shared.util.time.Full +import com.mooncloak.vpn.app.shared.util.time.format +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AppDetailsBottomSheetLayout( + details: SettingsAppDetails, + modifier: Modifier = Modifier, + dateTimeFormatter: DateTimeFormatter = remember { DateTimeFormatter.Full } +) { + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Column( + modifier = Modifier.fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(Res.string.settings_app_details_header), + style = MaterialTheme.typography.titleLarge + ) + + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_id)) + }, + supportingContent = { + Text( + text = details.id, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_name)) + }, + supportingContent = { + Text( + text = details.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_version)) + }, + supportingContent = { + Text( + text = details.version, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + + if (details.isDebug) { + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_debug)) + }, + supportingContent = { + Text( + text = stringResource(Res.string.global_yes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + } + + if (details.isPreRelease) { + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_pre_release)) + }, + supportingContent = { + Text( + text = stringResource(Res.string.global_yes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + } + + ListItem( + modifier = Modifier.fillMaxWidth(), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface + ), + headlineContent = { + Text(text = stringResource(Res.string.settings_app_details_title_build_time)) + }, + supportingContent = { + Text( + text = details.buildTime?.let { dateTimeFormatter.format(it) } + ?: stringResource(Res.string.global_not_available), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = SecondaryAlpha) + ) + } + ) + } + } +} diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsAppGroup.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsAppGroup.kt index 03390e1e..03c651d3 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsAppGroup.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsAppGroup.kt @@ -30,6 +30,8 @@ import org.jetbrains.compose.resources.stringResource internal fun ColumnScope.SettingsAppGroup( appVersion: String?, sourceCodeUri: String?, + appDetailsEnabled: Boolean = false, + onOpenAppDetails: () -> Unit, onOpenDependencyList: () -> Unit, onOpenCollaboratorList: () -> Unit, uriHandler: UriHandler = LocalUriHandler.current @@ -41,7 +43,10 @@ internal fun ColumnScope.SettingsAppGroup( ) ListItem( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() + .clickable(enabled = appDetailsEnabled) { + onOpenAppDetails.invoke() + }, colors = ListItemDefaults.colors( containerColor = MaterialTheme.colorScheme.background ), diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsBottomSheet.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsBottomSheet.kt index 32c00e1c..aef765aa 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsBottomSheet.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/composable/SettingsBottomSheet.kt @@ -38,6 +38,11 @@ internal fun SettingsBottomSheet( is SettingsBottomSheetDestination.Collaborators -> CollaboratorContainerScreen( modifier = Modifier.fillMaxWidth() ) + + is SettingsBottomSheetDestination.AppInfo -> AppDetailsBottomSheetLayout( + modifier = Modifier.fillMaxWidth(), + details = destination.details + ) } } } diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsAppDetails.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsAppDetails.kt new file mode 100644 index 00000000..7fa89939 --- /dev/null +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsAppDetails.kt @@ -0,0 +1,17 @@ +package com.mooncloak.vpn.app.shared.feature.settings.model + +import androidx.compose.runtime.Immutable +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +public data class SettingsAppDetails public constructor( + @SerialName(value = "id") val id: String, + @SerialName(value = "name") val name: String, + @SerialName(value = "version") val version: String, + @SerialName(value = "debug") val isDebug: Boolean, + @SerialName(value = "pre_release") val isPreRelease: Boolean, + @SerialName(value = "build_time") val buildTime: Instant? +) diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsBottomSheetDestination.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsBottomSheetDestination.kt index bf97452d..c2c3baa2 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsBottomSheetDestination.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/feature/settings/model/SettingsBottomSheetDestination.kt @@ -1,6 +1,7 @@ package com.mooncloak.vpn.app.shared.feature.settings.model import androidx.compose.runtime.Immutable +import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -23,6 +24,13 @@ internal sealed interface SettingsBottomSheetDestination { @SerialName(value = "plan") data object SelectPlan : SettingsBottomSheetDestination + @Immutable + @Serializable + @SerialName(value = "app_info") + data class AppInfo internal constructor( + val details: SettingsAppDetails + ) : SettingsBottomSheetDestination + @Immutable @Serializable @SerialName(value = "collaborators") diff --git a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/info/AppClientInfo.kt b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/info/AppClientInfo.kt index fdd3b7ca..057e810f 100644 --- a/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/info/AppClientInfo.kt +++ b/app-shared/src/commonMain/kotlin/com/mooncloak/vpn/app/shared/info/AppClientInfo.kt @@ -25,8 +25,8 @@ public interface AppClientInfo { public val versionName: String get() = "$version-$versionCode" - public val buildTime: Instant - get() = Instant.parse(SharedBuildConfig.appBuildTime) + public val buildTime: Instant? + get() = runCatching { Instant.parse(SharedBuildConfig.appBuildTime) }.getOrNull() public val flavor: String?