diff --git a/app/src/main/java/chat/stoat/screens/about/AboutScreen.kt b/app/src/main/java/chat/stoat/screens/about/AboutScreen.kt index 36dcc5b6..25b5b3f3 100644 --- a/app/src/main/java/chat/stoat/screens/about/AboutScreen.kt +++ b/app/src/main/java/chat/stoat/screens/about/AboutScreen.kt @@ -3,43 +3,39 @@ package chat.stoat.screens.about import android.content.ClipData import android.os.Build import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize 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.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources -import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.toClipEntry import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -47,6 +43,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel @@ -54,80 +52,34 @@ import androidx.navigation.NavController import chat.stoat.BuildConfig import chat.stoat.R import chat.stoat.api.StoatJson -import chat.stoat.api.routes.misc.Root import chat.stoat.api.routes.misc.getRootRoute -import chat.stoat.composables.generic.PrimaryTabs import chat.stoat.core.model.data.STOAT_BASE import chat.stoat.internals.Platform +import chat.stoat.settings.dsl.SettingsPage +import chat.stoat.ui.theme.FragmentMono import kotlinx.coroutines.launch import java.net.URI class AboutViewModel : ViewModel() { - var root by mutableStateOf(null) - var selectedTabIndex by mutableIntStateOf(0) - - fun getDebugInformation(): Map { - return mapOf( - "App Version" to BuildConfig.VERSION_NAME, - "App Type" to BuildConfig.FLAVOUR_ID, - "API Host" to URI(STOAT_BASE).host, - "API Version" to (root?.revolt ?: "Unknown"), - "Runtime SDK" to Build.VERSION.SDK_INT.toString(), - "Model" to "${Build.MANUFACTURER} ${ - Build.DEVICE.replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() - } - } (${Build.MODEL})" - ) - } + var debugInfo by mutableStateOf>(emptyMap()) + private set init { viewModelScope.launch { - root = getRootRoute().copy() - } - } -} - -@Composable -fun VersionItem(key: String, value: String, modifier: Modifier = Modifier) { - Row(modifier) { - Text( - text = key, - color = MaterialTheme.colorScheme.onBackground.copy( - alpha = 1.0f - ), - style = MaterialTheme.typography.titleMedium.copy( - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold - ), - modifier = Modifier - .padding(horizontal = 2.5.dp, vertical = 2.5.dp) - ) - Text( - text = value, - color = MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.9f - ), - style = MaterialTheme.typography.titleMedium.copy( - textAlign = TextAlign.Center, - fontWeight = FontWeight.Normal - ), - modifier = Modifier - .padding(horizontal = 2.5.dp, vertical = 2.5.dp) - ) - } -} - -@Composable -fun DebugInfo(viewModel: AboutViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 30.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - viewModel.getDebugInformation().forEach { (key, value) -> - VersionItem(key, value) + runCatching { getRootRoute() }.getOrNull()?.let { + debugInfo = mapOf( + "App Version" to BuildConfig.VERSION_NAME, + "App Type" to BuildConfig.FLAVOUR_ID, + "API Host" to URI(STOAT_BASE).host, + "API Version" to it.revolt, + "Runtime SDK" to Build.VERSION.SDK_INT.toString(), + "Model" to "${Build.MANUFACTURER} ${ + Build.DEVICE.replaceFirstChar { + if (it.isLowerCase()) it.titlecase() else it.toString() + } + } (${Build.MODEL})" + ) + } } } } @@ -140,109 +92,150 @@ fun AboutScreen(navController: NavController, viewModel: AboutViewModel = viewMo val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.about), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - IconButton(onClick = { - navController.popBackStack() - }) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back_24dp), - contentDescription = stringResource(id = R.string.back) - ) - } - } + var debugInfoOpen by remember { mutableStateOf(false) } + val debugInfoChevronRotation by animateFloatAsState(if (debugInfoOpen) 270f else 90f) + + SettingsPage( + navController, + title = { + Text( + text = stringResource(id = R.string.about), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - }, - ) { pv -> - Box(Modifier.padding(pv)) { + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { Column( modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, + .padding(top = 16.dp) + .fillMaxWidth() + .padding(vertical = 32.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - PrimaryTabs( - tabs = listOf( - stringResource(R.string.about_tab_version), - stringResource(R.string.about_tab_details) - ), - currentIndex = viewModel.selectedTabIndex, - onTabSelected = { viewModel.selectedTabIndex = it } + Image( + painter = painterResource(R.drawable.stoat_logo_white), + contentDescription = stringResource(R.string.about_full_name), + colorFilter = ColorFilter.tint(LocalContentColor.current), + modifier = Modifier + .width(250.dp) ) + Spacer(modifier = Modifier.height(16.dp)) + Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .weight(1f), - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - if (viewModel.root == null) { - CircularProgressIndicator( - modifier = Modifier - .size(48.dp) + Text( + text = stringResource(R.string.about_full_name), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) + + Text( + text = BuildConfig.VERSION_NAME, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Normal + ), + textAlign = TextAlign.Center + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainer), + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .clickable { debugInfoOpen = !debugInfoOpen } + .fillMaxWidth() + .padding(24.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(0.7f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(R.string.about_section_debug_information), + style = MaterialTheme.typography.titleMedium ) - } else { - when (viewModel.selectedTabIndex) { - 0 -> { - Image( - painter = painterResource(R.drawable.stoat_logo_white), - contentDescription = stringResource(R.string.about_full_name), - colorFilter = ColorFilter.tint(LocalContentColor.current), - modifier = Modifier - .width(250.dp) - ) + Text( + text = stringResource(R.string.about_section_debug_information_description), + style = MaterialTheme.typography.bodySmall, + lineHeight = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalContentColor.current.copy(alpha = 0.7f) + ) + } + Spacer(Modifier.weight(1f)) + Icon( + painter = painterResource(R.drawable.ic_chevron_forward_24dp), + contentDescription = null, + tint = LocalContentColor.current.copy(alpha = 0.5f), + modifier = Modifier.rotate(debugInfoChevronRotation) + ) + } - Spacer(modifier = Modifier.height(16.dp)) + AnimatedVisibility(visible = debugInfoOpen) { + Column(Modifier.padding(horizontal = 8.dp)) { + Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(R.string.about_full_name), - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center - ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + viewModel.debugInfo.forEach { (key, value) -> + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = key, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + letterSpacing = 0.05.em, + textAlign = TextAlign.Start + ) + ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(Modifier.weight(1f)) - Text( - text = BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.labelMedium.copy( - fontWeight = FontWeight.Normal - ), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Text( - text = stringResource(R.string.about_brought_to_you_by), - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Light - ), - color = LocalContentColor.current.copy( - alpha = 0.5f - ), - textAlign = TextAlign.Center - ) + Text( + text = value, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Light, + fontFamily = FragmentMono, + textAlign = TextAlign.End + ), + color = LocalContentColor.current.copy(alpha = 0.7f) + ) + } } - 1 -> { - DebugInfo(viewModel) - TextButton(onClick = { + Spacer(Modifier.height(16.dp)) + + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = { scope.launch { clipboard.setClipEntry( ClipData.newPlainText( "Stoat Debug Information", - StoatJson.encodeToString(viewModel.getDebugInformation()) + StoatJson.encodeToString(viewModel.debugInfo) ).toClipEntry() ) @@ -254,29 +247,67 @@ fun AboutScreen(navController: NavController, viewModel: AboutViewModel = viewMo ).show() } } - }) { - Text(text = stringResource(id = R.string.copy)) } + ) { + Icon( + painter = painterResource(R.drawable.ic_content_copy_24dp), + contentDescription = null + ) + + Spacer(Modifier.width(8.dp)) + + Text(text = stringResource(id = R.string.about_section_debug_information_copy_as_json)) } + + Spacer(Modifier.height(8.dp)) } } } + } - Column( + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainer), + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, modifier = Modifier + .clickable { navController.navigate("about/oss") } .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 30.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(24.dp) ) { - ElevatedButton( - onClick = { navController.navigate("about/oss") }, - modifier = Modifier - .testTag("view_oss_attribution") + Column( + modifier = Modifier.fillMaxWidth(0.7f), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Text(text = stringResource(id = R.string.oss_attribution)) + Text( + text = stringResource(R.string.about_section_oss_licenses), + style = MaterialTheme.typography.titleMedium + ) } + Spacer(Modifier.weight(1f)) + Icon( + painter = painterResource(R.drawable.ic_chevron_forward_24dp), + contentDescription = null, + tint = LocalContentColor.current.copy(alpha = 0.5f) + ) } } + + Text( + text = stringResource(R.string.about_brought_to_you_by), + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Light + ), + color = LocalContentColor.current.copy( + alpha = 0.5f + ), + textAlign = TextAlign.Center + ) } } } diff --git a/app/src/main/res/drawable/ic_chevron_forward_24dp.xml b/app/src/main/res/drawable/ic_chevron_forward_24dp.xml new file mode 100644 index 00000000..3d036ecb --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_forward_24dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b628dd6e..c203672d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,8 +87,10 @@ About Stoat for Android Brought to you with ❤️ by the Stoat team. - Version - Details + Debug Information + If you\'re experiencing issues with the app, this information may be helpful to include when contacting support or submitting a bug report. + Copy as JSON + Open Source Licenses OSS Licenses Tap to view license