diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/appicon/AppIconSelectionFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/appicon/AppIconSelectionFragment.kt index 8be069c68b..7b9490348f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/appicon/AppIconSelectionFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/appicon/AppIconSelectionFragment.kt @@ -87,235 +87,233 @@ class AppIconSelectionFragment : ComposeFragment() { findNavController().safeNavigate(R.id.action_appIconSelectionFragment_to_appIconTutorialFragment) } - /** - * Screen allowing the user to view all the possible icon and select a new one to use. - */ - @Composable - fun IconSelectionScreen(activeIcon: AppIconPreset, onItemConfirmed: (AppIconPreset) -> Unit, onWarningClick: () -> Unit, modifier: Modifier = Modifier) { - var showDialog: Boolean by remember { mutableStateOf(false) } - var pendingIcon: AppIconPreset by remember { - mutableStateOf(activeIcon) - } - - if (showDialog) { - ChangeIconDialog( - pendingIcon = pendingIcon, - onConfirm = { - onItemConfirmed(pendingIcon) - showDialog = false - }, - onDismiss = { - pendingIcon = activeIcon - showDialog = false - } - ) - } - - Column(modifier = modifier.verticalScroll(rememberScrollState())) { - Spacer(modifier = Modifier.size(12.dp)) - CaveatWarning( - onClick = onWarningClick, - modifier = Modifier.padding(horizontal = 24.dp) - ) - Spacer(modifier = Modifier.size(12.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 18.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - enumValues().toList().chunked(COLUMN_COUNT).map { it.toImmutableList() }.forEach { items -> - IconRow( - presets = items, - isSelected = { it == pendingIcon }, - onItemClick = { - pendingIcon = it - showDialog = true - } - ) - } - } - } - } - - @Composable - fun ChangeIconDialog(pendingIcon: AppIconPreset, onConfirm: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier) { - AlertDialog( - modifier = modifier, - onDismissRequest = onDismiss, - confirmButton = { - TextButton( - onClick = onConfirm - ) { - Text(text = stringResource(id = R.string.preferences__app_icon_dialog_ok)) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss - ) { - Text(text = stringResource(id = R.string.preferences__app_icon_dialog_cancel)) - } - }, - icon = { - AppIcon(preset = pendingIcon, isSelected = false, onClick = {}) - }, - title = { - Text( - text = stringResource(id = R.string.preferences__app_icon_dialog_title, stringResource(id = pendingIcon.labelResId)), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - Text( - text = stringResource(id = R.string.preferences__app_icon_dialog_description), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium - ) - } - ) - } - - /** - * Composable rendering the one row of icons that the user may choose from. - */ - @Composable - fun IconRow(presets: ImmutableList, isSelected: (AppIconPreset) -> Boolean, onItemClick: (AppIconPreset) -> Unit, modifier: Modifier = Modifier) { - Row(modifier = modifier.fillMaxWidth()) { - presets.forEach { preset -> - val currentlySelected = isSelected(preset) - IconGridElement( - preset = preset, - isSelected = currentlySelected, - onClickHandler = { - if (!currentlySelected) { - onItemClick(preset) - } - }, - modifier = Modifier - .padding(vertical = 18.dp) - .weight(1f) - ) - } - } - } - - /** - * Composable rendering an individual icon inside that grid, including the black border of the selected icon. - */ - @Composable - fun IconGridElement(preset: AppIconPreset, isSelected: Boolean, onClickHandler: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - val boxModifier = Modifier.size(64.dp) - Box( - modifier = if (isSelected) boxModifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else boxModifier - ) { - AppIcon(preset = preset, isSelected = isSelected, onClickHandler, modifier = Modifier.align(Alignment.Center)) - } - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(id = preset.labelResId), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - /** - * Composable rendering the multiple layers of an adaptive icon onto one flattened rasterized Canvas. - */ - @Composable - fun AppIcon(preset: AppIconPreset, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - val bitmapSize by animateFloatAsState(if (isSelected) 48f else 64f, label = "Icon Size") - val imageModifier = modifier - .size(bitmapSize.dp) - .graphicsLayer( - shape = CircleShape, - shadowElevation = if (isSelected) 4f else 8f, - clip = true - ) - .clickable(onClick = onClick) - Image( - painterResource(id = preset.iconPreviewResId), - contentDescription = stringResource(id = preset.labelResId), - modifier = imageModifier - ) - } - - /** - * A clickable "learn more" block of text. - */ - @Composable - fun CaveatWarning(onClick: () -> Unit, modifier: Modifier = Modifier) { - val learnMoreString = stringResource(R.string.preferences__app_icon_learn_more) - val completeString = stringResource(R.string.preferences__app_icon_warning_learn_more) - val learnMoreStartIndex = completeString.indexOf(learnMoreString).coerceAtLeast(0) - val learnMoreEndIndex = learnMoreStartIndex + learnMoreString.length - val doesStringEndWithLearnMore = learnMoreEndIndex >= completeString.lastIndex - val annotatedText = buildAnnotatedString { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - append(completeString.substring(0, learnMoreStartIndex)) - } - pushStringAnnotation( - tag = URL_TAG, - annotation = LEARN_MORE_TAG - ) - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary - ) - ) { - append(learnMoreString) - } - pop() - if (!doesStringEndWithLearnMore) { - append(completeString.substring(learnMoreEndIndex, completeString.lastIndex)) - } - } - ClickableText( - text = annotatedText, - onClick = { _ -> - onClick() - }, - style = MaterialTheme.typography.bodyMedium, - modifier = modifier - ) - } - - @Preview(name = "Light Theme") - @Composable - private fun MainScreenPreviewLight() { - SignalTheme(isDarkMode = false) { - Surface { - IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {}) - } - } - } - - @Preview(name = "Dark Theme") - @Composable - private fun MainScreenPreviewDark() { - SignalTheme(isDarkMode = true) { - Surface { - IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {}) - } - } - } - companion object { val TAG = Log.tag(AppIconSelectionFragment::class.java) - - private const val LEARN_MORE_TAG = "learn_more" - private const val URL_TAG = "URL" - private const val COLUMN_COUNT = 4 + } +} + +private const val LEARN_MORE_TAG = "learn_more" +private const val URL_TAG = "URL" +private const val COLUMN_COUNT = 4 + +/** + * Screen allowing the user to view all the possible icon and select a new one to use. + */ +@Composable +fun IconSelectionScreen(activeIcon: AppIconPreset, onItemConfirmed: (AppIconPreset) -> Unit, onWarningClick: () -> Unit, modifier: Modifier = Modifier) { + var showDialog: Boolean by remember { mutableStateOf(false) } + var pendingIcon: AppIconPreset by remember { + mutableStateOf(activeIcon) + } + + if (showDialog) { + ChangeIconDialog( + pendingIcon = pendingIcon, + onConfirm = { + onItemConfirmed(pendingIcon) + showDialog = false + }, + onDismiss = { + pendingIcon = activeIcon + showDialog = false + } + ) + } + + Column(modifier = modifier.verticalScroll(rememberScrollState())) { + Spacer(modifier = Modifier.size(12.dp)) + CaveatWarning( + onClick = onWarningClick, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Spacer(modifier = Modifier.size(12.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + enumValues().toList().chunked(COLUMN_COUNT).map { it.toImmutableList() }.forEach { items -> + IconRow( + presets = items, + isSelected = { it == pendingIcon }, + onItemClick = { + pendingIcon = it + showDialog = true + } + ) + } + } + } +} + +@Composable +fun ChangeIconDialog(pendingIcon: AppIconPreset, onConfirm: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier) { + AlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = onConfirm + ) { + Text(text = stringResource(id = R.string.preferences__app_icon_dialog_ok)) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss + ) { + Text(text = stringResource(id = R.string.preferences__app_icon_dialog_cancel)) + } + }, + icon = { + AppIcon(preset = pendingIcon, isSelected = false, onClick = {}) + }, + title = { + Text( + text = stringResource(id = R.string.preferences__app_icon_dialog_title, stringResource(id = pendingIcon.labelResId)), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + Text( + text = stringResource(id = R.string.preferences__app_icon_dialog_description), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + } + ) +} + +/** + * Composable rendering the one row of icons that the user may choose from. + */ +@Composable +fun IconRow(presets: ImmutableList, isSelected: (AppIconPreset) -> Boolean, onItemClick: (AppIconPreset) -> Unit, modifier: Modifier = Modifier) { + Row(modifier = modifier.fillMaxWidth()) { + presets.forEach { preset -> + val currentlySelected = isSelected(preset) + IconGridElement( + preset = preset, + isSelected = currentlySelected, + onClickHandler = { + if (!currentlySelected) { + onItemClick(preset) + } + }, + modifier = Modifier + .padding(vertical = 18.dp) + .weight(1f) + ) + } + } +} + +/** + * Composable rendering an individual icon inside that grid, including the black border of the selected icon. + */ +@Composable +fun IconGridElement(preset: AppIconPreset, isSelected: Boolean, onClickHandler: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val boxModifier = Modifier.size(64.dp) + Box( + modifier = if (isSelected) boxModifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else boxModifier + ) { + AppIcon(preset = preset, isSelected = isSelected, onClickHandler, modifier = Modifier.align(Alignment.Center)) + } + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = preset.labelResId), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +/** + * Composable rendering the icon and optionally a border, to indicate selection. + */ +@Composable +fun AppIcon(preset: AppIconPreset, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val bitmapSize by animateFloatAsState(if (isSelected) 48f else 64f, label = "Icon Size") + val imageModifier = modifier + .size(bitmapSize.dp) + .graphicsLayer( + shape = CircleShape, + shadowElevation = if (isSelected) 4f else 8f, + clip = true + ) + .clickable(onClick = onClick) + Image( + painterResource(id = preset.iconPreviewResId), + contentDescription = stringResource(id = preset.labelResId), + modifier = imageModifier + ) +} + +/** + * A clickable "learn more" block of text. + */ +@Composable +fun CaveatWarning(onClick: () -> Unit, modifier: Modifier = Modifier) { + val learnMoreString = stringResource(R.string.preferences__app_icon_learn_more) + val completeString = stringResource(R.string.preferences__app_icon_warning_learn_more) + val learnMoreStartIndex = completeString.indexOf(learnMoreString).coerceAtLeast(0) + val learnMoreEndIndex = learnMoreStartIndex + learnMoreString.length + val doesStringEndWithLearnMore = learnMoreEndIndex >= completeString.lastIndex + val annotatedText = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + append(completeString.substring(0, learnMoreStartIndex)) + } + pushStringAnnotation( + tag = URL_TAG, + annotation = LEARN_MORE_TAG + ) + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary + ) + ) { + append(learnMoreString) + } + pop() + if (!doesStringEndWithLearnMore) { + append(completeString.substring(learnMoreEndIndex, completeString.lastIndex)) + } + } + ClickableText( + text = annotatedText, + onClick = { onClick() }, + style = MaterialTheme.typography.bodyMedium, + modifier = modifier + ) +} + +@Preview(name = "Light Theme") +@Composable +private fun MainScreenPreviewLight() { + SignalTheme(isDarkMode = false) { + Surface { + IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {}) + } + } +} + +@Preview(name = "Dark Theme") +@Composable +private fun MainScreenPreviewDark() { + SignalTheme(isDarkMode = true) { + Surface { + IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {}) + } } }