diff --git a/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/history/ChangelogE2ETest.kt b/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/history/ChangelogE2ETest.kt index 321ac34..3599919 100644 --- a/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/history/ChangelogE2ETest.kt +++ b/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/history/ChangelogE2ETest.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToKey import com.google.common.truth.Truth import com.serranoie.app.minus.R import com.serranoie.app.minus.domain.model.PeriodMappingMode @@ -95,6 +95,8 @@ class ChangelogE2ETest { onNotificationTimeChange = { _, _ -> }, onRecurrentNotificationTimeChange = { _, _ -> }, onOpenExactAlarmSettings = {}, + notificationPermissionGranted = true, + onOpenNotificationSettings = {}, periodMappingMode = PeriodMappingMode.ACTIVE_BUDGET, onPeriodMappingModeChange = {}, onExportCsv = {}, @@ -137,20 +139,6 @@ class ChangelogE2ETest { private fun closeContentDesc(): String = composeTestRule.activity.getString(R.string.changelog_close) - @Test - fun when_settings_is_displayed_then_what_is_new_item_is_visible() { - setSettingsContent() - composeTestRule.waitForIdle() - composeTestRule.mainClock.advanceTimeBy(500) - composeTestRule.waitForIdle() - - composeTestRule.onNodeWithTag("SettingsScreen").performScrollToIndex(Int.MAX_VALUE) - composeTestRule.waitForIdle() - - composeTestRule.onAllNodesWithText(whatsNewTitle()).onFirst().assertIsDisplayed() - composeTestRule.onAllNodesWithText(whatsNewSubtitle()).onFirst().assertIsDisplayed() - } - @Test fun when_tapping_what_is_new_then_onNavigateToChangelog_callback_fires() { var navigatedCount = 0 @@ -159,7 +147,8 @@ class ChangelogE2ETest { composeTestRule.mainClock.advanceTimeBy(500) composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag("SettingsScreen").performScrollToIndex(Int.MAX_VALUE) + composeTestRule.onNodeWithTag("SettingsScreen") + .performScrollToKey("settings_app_info_section") composeTestRule.waitForIdle() composeTestRule.onAllNodesWithText(whatsNewTitle()).onFirst().performClick() @@ -306,7 +295,8 @@ class ChangelogE2ETest { composeTestRule.mainClock.advanceTimeBy(500) composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag("SettingsScreen").performScrollToIndex(Int.MAX_VALUE) + composeTestRule.onNodeWithTag("SettingsScreen") + .performScrollToKey("settings_app_info_section") composeTestRule.waitForIdle() composeTestRule.onAllNodesWithText(whatsNewTitle()).assertCountEquals(1) diff --git a/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/home/MainScreenE2ETest.kt b/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/home/MainScreenE2ETest.kt index 49d2e45..898129c 100644 --- a/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/home/MainScreenE2ETest.kt +++ b/app/src/androidTest/java/com/serranoie/app/minus/presentation/ui/e2e/home/MainScreenE2ETest.kt @@ -235,7 +235,9 @@ class MainScreenE2ETest { onDelete = { capturedIntents += "Delete" }, onApply = { capturedIntents += "Apply" }, onOperatorInput = { capturedIntents += "Operator:$it" }, - modifier = Modifier.fillMaxSize().height(400.dp), + modifier = Modifier + .fillMaxSize() + .height(400.dp), ) } } @@ -333,7 +335,8 @@ class MainScreenE2ETest { animState = AnimState.EDITING, ) - composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("123"))).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("123"))).onLast() + .assertIsDisplayed() } @Test @@ -401,7 +404,8 @@ class MainScreenE2ETest { budgetSettings = sampleBudgetSettings(totalBudget = BigDecimal("500.00")), ) - composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("500.00"))).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("500.00"))).onLast() + .assertIsDisplayed() } @Test @@ -417,7 +421,8 @@ class MainScreenE2ETest { budgetSettings = sampleBudgetSettings(totalBudget = BigDecimal("500.00")), ) - composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("450.00"))).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("450.00"))).onLast() + .assertIsDisplayed() } @Test @@ -437,7 +442,8 @@ class MainScreenE2ETest { viewPeriod = BudgetPeriod.WEEKLY, ) - composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("700.00"))).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("700.00"))).onLast() + .assertIsDisplayed() } @Test @@ -556,7 +562,8 @@ class MainScreenE2ETest { animState = AnimState.EDITING, ) - composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("123"))).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(formatExpected(BigDecimal("123"))).onLast() + .assertIsDisplayed() composeTestRule.onAllNodesWithText("1", substring = true).onLast().assertIsDisplayed() composeTestRule.onAllNodesWithText("2", substring = true).onLast().assertIsDisplayed() composeTestRule.onAllNodesWithText("3", substring = true).onLast().assertIsDisplayed() @@ -700,6 +707,7 @@ class MainScreenE2ETest { composeTestRule.mainClock.advanceTimeBy(300) val totalBudgetLabel = composeTestRule.activity.getString(R.string.total_budget) - composeTestRule.onAllNodesWithText(totalBudgetLabel, substring = true).onLast().assertIsDisplayed() + composeTestRule.onAllNodesWithText(totalBudgetLabel, substring = true).onLast() + .assertIsDisplayed() } } diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/Settings.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/Settings.kt index 3598183..eda59ac 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/Settings.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/Settings.kt @@ -2,13 +2,13 @@ package com.serranoie.app.minus.presentation.ui.settings -import android.app.TimePickerDialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -26,6 +26,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.Alarm +import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.Backup import androidx.compose.material.icons.filled.Brightness4 import androidx.compose.material.icons.filled.BugReport @@ -40,7 +41,6 @@ import androidx.compose.material.icons.filled.Publish import androidx.compose.material.icons.filled.QuestionMark import androidx.compose.material.icons.filled.Repeat import androidx.compose.material.icons.filled.TextFields -import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.outlined.RemoveRedEye import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -59,10 +59,12 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTimePickerState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -87,6 +89,7 @@ import com.serranoie.app.minus.R import com.serranoie.app.minus.domain.model.PeriodMappingMode import com.serranoie.app.minus.presentation.ui.history.RecurrentPaymentsViewMode import com.serranoie.app.minus.presentation.ui.settings.bugreport.buildAppEnvironmentMetadata +import com.serranoie.app.minus.presentation.ui.settings.components.NotificationPermissionItem import com.serranoie.app.minus.presentation.ui.theme.MinusTheme import com.serranoie.app.minus.presentation.ui.theme.bodySmallCondensed import com.serranoie.app.minus.presentation.ui.theme.component.CustomPaddedExpandableItem @@ -105,1030 +108,1114 @@ import java.util.Locale @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun Settings( - modifier: Modifier = Modifier, - isCensored: Boolean = false, - currentTheme: String, - currentTypography: String, - isMaterialYouEnabled: Boolean, - isCreditQuickToggleFeatureEnabled: Boolean, - recurrentPaymentsViewMode: RecurrentPaymentsViewMode, - notificationHour: Int, - notificationMinute: Int, - recurrentNotificationHour: Int, - recurrentNotificationMinute: Int, - exactAlarmEnabled: Boolean, - onThemeChange: (String) -> Unit, - onTypographyChange: (String) -> Unit, - onMaterialYouToggle: () -> Unit, - onCreditQuickToggleFeatureToggle: () -> Unit, - onRecurrentPaymentsViewModeChange: (RecurrentPaymentsViewMode) -> Unit, - onNotificationTimeChange: (Int, Int) -> Unit, - onRecurrentNotificationTimeChange: (Int, Int) -> Unit, - onOpenExactAlarmSettings: () -> Unit, - periodMappingMode: PeriodMappingMode, - onPeriodMappingModeChange: (PeriodMappingMode) -> Unit, - onExportCsv: () -> Unit = {}, - onImportCsv: () -> Unit = {}, - onResetTutorial: () -> Unit = {}, - onBugReportClick: () -> Unit = {}, - onNavigateToChangelog: () -> Unit = {}, - onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + isCensored: Boolean = false, + currentTheme: String, + currentTypography: String, + isMaterialYouEnabled: Boolean, + isCreditQuickToggleFeatureEnabled: Boolean, + recurrentPaymentsViewMode: RecurrentPaymentsViewMode, + notificationHour: Int, + notificationMinute: Int, + recurrentNotificationHour: Int, + recurrentNotificationMinute: Int, + exactAlarmEnabled: Boolean, + notificationPermissionGranted: Boolean, + onThemeChange: (String) -> Unit, + onTypographyChange: (String) -> Unit, + onMaterialYouToggle: () -> Unit, + onCreditQuickToggleFeatureToggle: () -> Unit, + onRecurrentPaymentsViewModeChange: (RecurrentPaymentsViewMode) -> Unit, + onNotificationTimeChange: (Int, Int) -> Unit, + onRecurrentNotificationTimeChange: (Int, Int) -> Unit, + onOpenExactAlarmSettings: () -> Unit, + onOpenNotificationSettings: () -> Unit, + periodMappingMode: PeriodMappingMode, + onPeriodMappingModeChange: (PeriodMappingMode) -> Unit, + onExportCsv: () -> Unit = {}, + onImportCsv: () -> Unit = {}, + onResetTutorial: () -> Unit = {}, + onBugReportClick: () -> Unit = {}, + onNavigateToChangelog: () -> Unit = {}, + onBack: () -> Unit = {}, ) { - var showThemeDialog by remember { mutableStateOf(false) } - var showTypographyDialog by remember { mutableStateOf(false) } - var showRecurrentPaymentsViewModeDialog by remember { mutableStateOf(false) } - var showNotificationTimePicker by remember { mutableStateOf(false) } - var showRecurrentNotificationTimePicker by remember { mutableStateOf(false) } - var isCreditFeatureExpanded by remember { mutableStateOf(false) } - val dismissThemeDialog = { showThemeDialog = false } - val dismissTypographyDialog = { showTypographyDialog = false } - val dismissRecurrentPaymentsViewModeDialog = { showRecurrentPaymentsViewModeDialog = false } - val dismissNotificationTimePicker = { showNotificationTimePicker = false } - val dismissRecurrentNotificationTimePicker = { showRecurrentNotificationTimePicker = false } - val scrollBehavior = - TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current - val view = LocalView.current - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - val appVersionName = "v${BuildConfig.VERSION_NAME}" - val metadataCopiedMessage = stringResource(R.string.settings_version_metadata_copied) + var showThemeDialog by remember { mutableStateOf(false) } + var showTypographyDialog by remember { mutableStateOf(false) } + var showRecurrentPaymentsViewModeDialog by remember { mutableStateOf(false) } + var showNotificationTimePicker by remember { mutableStateOf(false) } + var showRecurrentNotificationTimePicker by remember { mutableStateOf(false) } + var isCreditFeatureExpanded by remember { mutableStateOf(false) } + val dismissThemeDialog = { showThemeDialog = false } + val dismissTypographyDialog = { showTypographyDialog = false } + val dismissRecurrentPaymentsViewModeDialog = { showRecurrentPaymentsViewModeDialog = false } + val dismissNotificationTimePicker = { showNotificationTimePicker = false } + val dismissRecurrentNotificationTimePicker = { showRecurrentNotificationTimePicker = false } + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + val context = LocalContext.current + val view = LocalView.current + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val appVersionName = "v${BuildConfig.VERSION_NAME}" + val metadataCopiedMessage = stringResource(R.string.settings_version_metadata_copied) - Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - topBar = { - MediumTopAppBar( - title = { - Text( - text = stringResource(R.string.settings_title), - style = MaterialTheme.typography.titleLargeEmphasized, - ) - }, navigationIcon = { - IconButton( - onClick = onBack, modifier = Modifier.testTag("SettingsBackButton") - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null - ) - } - }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface - ), scrollBehavior = scrollBehavior - ) - }) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .testTag("SettingsScreen"), - ) { - if (isCensored) { - item { - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.large, - border = BorderStroke( - 1.dp, - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) - ), - colors = CardDefaults.outlinedCardColors( - containerColor = Color.Transparent - ), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 14.dp), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Outlined.RemoveRedEye, - contentDescription = null, - tint = MaterialTheme.colorScheme.outline, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.censor_mode_card_label), - style = MaterialTheme.typography.bodySmallCondensed, - color = MaterialTheme.colorScheme.outline, - ) - } - Text( - text = stringResource(R.string.censor_mode_card_body), - modifier = Modifier.padding(top = 4.dp), - style = MaterialTheme.typography.bodySmallCondensed, - color = MaterialTheme.colorScheme.onSurface, - ) - } - } - } - } - item { - PaddedListGroup( - title = stringResource(R.string.settings_section_appearance) - ) { - CustomPaddedListItem( - onClick = { - showThemeDialog = true - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.First, - modifier = Modifier.testTag("SettingsThemeItem") - ) { - Icon( - imageVector = Icons.Default.Brightness4, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Spacer(modifier = Modifier.width(16.dp)) + Scaffold( + modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + MediumTopAppBar( + title = { + Text( + text = stringResource(R.string.settings_title), + style = MaterialTheme.typography.titleLargeEmphasized, + ) + }, + navigationIcon = { + IconButton( + onClick = onBack, + modifier = Modifier.testTag("SettingsBackButton") + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ), + scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .testTag("SettingsScreen"), + ) { + if (isCensored) { + item { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = MaterialTheme.shapes.large, + border = BorderStroke( + 1.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 14.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Outlined.RemoveRedEye, + contentDescription = null, + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.censor_mode_card_label), + style = MaterialTheme.typography.bodySmallCondensed, + color = MaterialTheme.colorScheme.outline, + ) + } + Text( + text = stringResource(R.string.censor_mode_card_body), + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmallCondensed, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } + } + item { + PaddedListGroup( + title = stringResource(R.string.settings_section_appearance) + ) { + CustomPaddedListItem( + onClick = { + showThemeDialog = true + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.First, + modifier = Modifier.testTag("SettingsThemeItem") + ) { + Icon( + imageVector = Icons.Default.Brightness4, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Spacer(modifier = Modifier.width(16.dp)) - Text( - text = stringResource(R.string.settings_theme_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_theme_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = currentTheme, - style = MaterialTheme.typography.labelLargeCondensed, - color = MaterialTheme.colorScheme.primary - ) - } + Text( + text = stringResource(R.string.settings_theme_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_theme_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = currentTheme, + style = MaterialTheme.typography.labelLargeCondensed, + color = MaterialTheme.colorScheme.primary + ) + } - CustomPaddedListItem( - onClick = { - showTypographyDialog = true - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.Middle, - modifier = Modifier.testTag("SettingsTypoItem") - ) { - Icon( - imageVector = Icons.Default.TextFields, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) + CustomPaddedListItem( + onClick = { + showTypographyDialog = true + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Middle, + modifier = Modifier.testTag("SettingsTypoItem") + ) { + Icon( + imageVector = Icons.Default.TextFields, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Spacer(modifier = Modifier.width(16.dp)) - Text( - text = stringResource(R.string.settings_typography_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_typography_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = currentTypography, - style = MaterialTheme.typography.labelLargeCondensed, - color = MaterialTheme.colorScheme.primary - ) - } + Text( + text = stringResource(R.string.settings_typography_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_typography_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = currentTypography, + style = MaterialTheme.typography.labelLargeCondensed, + color = MaterialTheme.colorScheme.primary + ) + } - CustomPaddedListItem( - onClick = onMaterialYouToggle, - position = PaddedListItemPosition.Last, - modifier = Modifier.testTag("SettingsMaterialYouItem") - ) { - Icon( - imageVector = Icons.Default.Palette, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_material_you_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_material_you_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(end = 4.dp) - ) - } - Switch( - checked = isMaterialYouEnabled, onCheckedChange = { - onMaterialYouToggle() -// view.toggleFeedback() - }, modifier = Modifier.testTag("SettingsMaterialYouSwitch") - ) - } - } - } + CustomPaddedListItem( + onClick = onMaterialYouToggle, + position = PaddedListItemPosition.Last, + modifier = Modifier.testTag("SettingsMaterialYouItem") + ) { + Icon( + imageVector = Icons.Default.Palette, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_material_you_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_material_you_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(end = 4.dp) + ) + } + Switch( + checked = isMaterialYouEnabled, + onCheckedChange = { + onMaterialYouToggle() +// view.toggleFeedback() + }, + modifier = Modifier.testTag("SettingsMaterialYouSwitch") + ) + } + } + } - item { - PaddedListGroup( - title = stringResource(R.string.settings_section_features) - ) { - CustomPaddedExpandableItem( - isExpanded = isCreditFeatureExpanded, - onToggleExpanded = { isCreditFeatureExpanded = !isCreditFeatureExpanded }, - position = PaddedListItemPosition.First, - modifier = Modifier.testTag("SettingsCreditQuickToggleFeatureItem"), - defaultContent = { - Icon( - imageVector = Icons.Default.CreditCard, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_feature_credit_toggle_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_feature_credit_toggle_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = if (isCreditFeatureExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - expandedContent = { - Text( - text = stringResource(R.string.settings_feature_credit_toggle_details), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, start = 6.dp, end = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.settings_feature_credit_toggle_switch_label), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f) - ) - Switch( - checked = isCreditQuickToggleFeatureEnabled, - onCheckedChange = { onCreditQuickToggleFeatureToggle() }, - modifier = Modifier.testTag("SettingsCreditQuickToggleFeatureSwitch") - ) - } - } - ) + item { + PaddedListGroup( + title = stringResource(R.string.settings_section_features) + ) { + CustomPaddedExpandableItem( + isExpanded = isCreditFeatureExpanded, + onToggleExpanded = { isCreditFeatureExpanded = !isCreditFeatureExpanded }, + position = PaddedListItemPosition.First, + modifier = Modifier.testTag("SettingsCreditQuickToggleFeatureItem"), + defaultContent = { + Icon( + imageVector = Icons.Default.CreditCard, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_feature_credit_toggle_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_feature_credit_toggle_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = if (isCreditFeatureExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + expandedContent = { + Text( + text = stringResource(R.string.settings_feature_credit_toggle_details), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, start = 6.dp, end = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.settings_feature_credit_toggle_switch_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + Switch( + checked = isCreditQuickToggleFeatureEnabled, + onCheckedChange = { onCreditQuickToggleFeatureToggle() }, + modifier = Modifier.testTag("SettingsCreditQuickToggleFeatureSwitch") + ) + } + } + ) - CustomPaddedListItem( - onClick = { - showRecurrentPaymentsViewModeDialog = true - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.Last, - modifier = Modifier.testTag("SettingsRecurrentPaymentsViewModeItem") - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ViewList, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_recurrent_payments_view_mode_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_recurrent_payments_view_mode_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = recurrentPaymentsViewMode.label(), - style = MaterialTheme.typography.labelLargeCondensed, - color = MaterialTheme.colorScheme.primary - ) - } - } - } + CustomPaddedListItem( + onClick = { + showRecurrentPaymentsViewModeDialog = true + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Last, + modifier = Modifier.testTag("SettingsRecurrentPaymentsViewModeItem") + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ViewList, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_recurrent_payments_view_mode_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_recurrent_payments_view_mode_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = recurrentPaymentsViewMode.label(), + style = MaterialTheme.typography.labelLargeCondensed, + color = MaterialTheme.colorScheme.primary + ) + } + } + } - item { - PaddedListGroup( - title = stringResource(R.string.settings_section_notifications) - ) { - CustomPaddedListItem( - onClick = { - showNotificationTimePicker = true - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.First, - ) { - Icon( - imageVector = Icons.Default.AccessTime, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_period_end_time_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_period_end_time_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = formatNotificationTime( - context, notificationHour, notificationMinute - ), - style = MaterialTheme.typography.labelLargeCondensed, - color = MaterialTheme.colorScheme.primary - ) - } + item { + PaddedListGroup( + title = stringResource(R.string.settings_section_notifications) + ) { + NotificationPermissionItem( + granted = notificationPermissionGranted, + onClick = onOpenNotificationSettings, + position = PaddedListItemPosition.First, + ) - CustomPaddedListItem( - onClick = { - showRecurrentNotificationTimePicker = true - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.Middle, - ) { - Icon( - imageVector = Icons.Default.Repeat, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_recurrent_notification_time_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_recurrent_notification_time_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = formatNotificationTime( - context, recurrentNotificationHour, recurrentNotificationMinute - ), - style = MaterialTheme.typography.labelLargeCondensed, - color = MaterialTheme.colorScheme.primary - ) - } + CustomPaddedListItem( + onClick = { + showNotificationTimePicker = true + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Middle, + ) { + Icon( + imageVector = Icons.Default.AccessTime, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_period_end_time_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_period_end_time_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = formatNotificationTime( + context, + notificationHour, + notificationMinute + ), + style = MaterialTheme.typography.labelLargeCondensed, + color = MaterialTheme.colorScheme.primary + ) + } - CustomPaddedListItem( - onClick = { - onOpenExactAlarmSettings() - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.Last, - ) { - Icon( - imageVector = Icons.Default.Alarm, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_exact_alarm_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = if (exactAlarmEnabled) { - stringResource(R.string.settings_exact_alarm_enabled_subtitle) - } else { - stringResource(R.string.settings_exact_alarm_disabled_subtitle) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } + CustomPaddedListItem( + onClick = { + showRecurrentNotificationTimePicker = true + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Middle, + ) { + Icon( + imageVector = Icons.Default.Repeat, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_recurrent_notification_time_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_recurrent_notification_time_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = formatNotificationTime( + context, + recurrentNotificationHour, + recurrentNotificationMinute + ), + style = MaterialTheme.typography.labelLargeCondensed, + color = MaterialTheme.colorScheme.primary + ) + } - item { - PaddedListGroup( - title = stringResource(R.string.settings_section_data_backup) - ) { - CustomPaddedListItem( - onClick = { - onExportCsv() - view.toggleFeedback() - }, position = PaddedListItemPosition.First - ) { - Icon( - imageVector = Icons.Default.Backup, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_backup_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_backup_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + CustomPaddedListItem( + onClick = { + onOpenExactAlarmSettings() + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Last, + borderStroke = if (!exactAlarmEnabled) { + BorderStroke(1.dp, MaterialTheme.colorScheme.error) + } else { + null + }, + ) { + Icon( + imageVector = Icons.Default.Alarm, + contentDescription = null, + tint = if (exactAlarmEnabled) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_exact_alarm_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = if (exactAlarmEnabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.error + } + ) + Text( + text = if (exactAlarmEnabled) { + stringResource(R.string.settings_exact_alarm_enabled_subtitle) + } else { + stringResource(R.string.settings_exact_alarm_disabled_subtitle) + }, + style = MaterialTheme.typography.bodySmall, + color = if (exactAlarmEnabled) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.error + } + ) + } + } + } + } - CustomPaddedListItem( - onClick = { - onImportCsv() - view.toggleFeedback() - }, position = PaddedListItemPosition.Last - ) { - Icon( - imageVector = Icons.Default.Publish, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_import_csv_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_import_csv_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } + item { + PaddedListGroup( + title = stringResource(R.string.settings_section_data_backup) + ) { + CustomPaddedListItem( + onClick = { + onExportCsv() + view.toggleFeedback() + }, + position = PaddedListItemPosition.First + ) { + Icon( + imageVector = Icons.Default.Backup, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_backup_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_backup_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } - item { - PaddedListGroup( - title = stringResource(R.string.settings_section_app_info) - ) { - CustomPaddedListItem( - onClick = { - onNavigateToChangelog() - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.First, - ) { - Icon( - imageVector = Icons.Default.AutoAwesome, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.changelog_settings_item_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource( - R.string.changelog_settings_item_subtitle, - BuildConfig.VERSION_NAME, - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + CustomPaddedListItem( + onClick = { + onImportCsv() + view.toggleFeedback() + }, + position = PaddedListItemPosition.Last + ) { + Icon( + imageVector = Icons.Default.Publish, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_import_csv_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_import_csv_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } - CustomPaddedListItem( - onClick = { - Utils.openWebLink(context, "https://www.github.com/isaacsa51/Minus") - view.weakHapticFeedback() - }, position = PaddedListItemPosition.Middle - ) { - Icon( - imageVector = Icons.Default.QuestionMark, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_about_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_about_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + item(key = "settings_app_info_section") { + PaddedListGroup( + title = stringResource(R.string.settings_section_app_info) + ) { + CustomPaddedListItem( + onClick = { + onNavigateToChangelog() + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.First, + ) { + Icon( + imageVector = Icons.Default.AutoAwesome, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.changelog_settings_item_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource( + R.string.changelog_settings_item_subtitle, + BuildConfig.VERSION_NAME, + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } - CustomPaddedListItem( - onClick = { - onBugReportClick() - view.weakHapticFeedback() - }, position = PaddedListItemPosition.Middle - ) { - Icon( - imageVector = Icons.Default.BugReport, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_bug_report_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(R.string.settings_bug_report_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + CustomPaddedListItem( + onClick = { + Utils.openWebLink(context, "https://www.github.com/isaacsa51/Minus") + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Middle + ) { + Icon( + imageVector = Icons.Default.QuestionMark, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_about_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_about_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + CustomPaddedListItem( + onClick = { + onBugReportClick() + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Middle + ) { + Icon( + imageVector = Icons.Default.BugReport, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_bug_report_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_bug_report_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } - CustomPaddedListItem( - onClick = { - view.weakHapticFeedback() - }, - position = PaddedListItemPosition.Last, - onLongClick = { - context.copyAppEnvironmentMetadataToClipboard() - view.toggleFeedback() - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = metadataCopiedMessage, - duration = SnackbarDuration.Short, - ) - } - }, - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.settings_version_title), - style = MaterialTheme.typography.bodyMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = appVersionName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } + CustomPaddedListItem( + onClick = { + view.weakHapticFeedback() + }, + position = PaddedListItemPosition.Last, + onLongClick = { + context.copyAppEnvironmentMetadataToClipboard() + view.toggleFeedback() + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = metadataCopiedMessage, + duration = SnackbarDuration.Short, + ) + } + }, + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_version_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = appVersionName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } // -// item { -// PaddedListGroup( -// title = stringResource(R.string.settings_section_tutorial) -// ) { -// CustomPaddedListItem( -// onClick = { -// onResetTutorial() -// view.toggleFeedback() -// }, -// position = PaddedListItemPosition.Single, -// modifier = Modifier.testTag("SettingsResetTutorialItem") -// ) { -// Icon( -// imageVector = Icons.Default.Refresh, -// contentDescription = null, -// tint = MaterialTheme.colorScheme.primary -// ) -// Spacer(modifier = Modifier.width(16.dp)) -// Column(modifier = Modifier.weight(1f)) { -// Text( -// text = stringResource(R.string.settings_reset_tutorial_title), -// style = MaterialTheme.typography.bodyMediumEmphasized, -// color = MaterialTheme.colorScheme.onSurface -// ) -// Text( -// text = stringResource(R.string.settings_reset_tutorial_subtitle), -// style = MaterialTheme.typography.bodySmall, -// color = MaterialTheme.colorScheme.onSurfaceVariant -// ) -// } -// } -// } -// } - } +// item { +// PaddedListGroup( +// title = stringResource(R.string.settings_section_tutorial) +// ) { +// CustomPaddedListItem( +// onClick = { +// onResetTutorial() +// view.toggleFeedback() +// }, +// position = PaddedListItemPosition.Single, +// modifier = Modifier.testTag("SettingsResetTutorialItem") +// ) { +// Icon( +// imageVector = Icons.Default.Refresh, +// contentDescription = null, +// tint = MaterialTheme.colorScheme.primary +// ) +// Spacer(modifier = Modifier.width(16.dp)) +// Column(modifier = Modifier.weight(1f)) { +// Text( +// text = stringResource(R.string.settings_reset_tutorial_title), +// style = MaterialTheme.typography.bodyMediumEmphasized, +// color = MaterialTheme.colorScheme.onSurface +// ) +// Text( +// text = stringResource(R.string.settings_reset_tutorial_subtitle), +// style = MaterialTheme.typography.bodySmall, +// color = MaterialTheme.colorScheme.onSurfaceVariant +// ) +// } +// } +// } +// } + } - if (showThemeDialog) { - ThemePickerDialog( - currentTheme = currentTheme, - onThemeSelected = onThemeChange, - onDismiss = dismissThemeDialog - ) - } + if (showThemeDialog) { + ThemePickerDialog( + currentTheme = currentTheme, + onThemeSelected = onThemeChange, + onDismiss = dismissThemeDialog + ) + } - if (showTypographyDialog) { - TypographyPickerDialog( - currentTypography = currentTypography, - onTypographySelected = onTypographyChange, - onDismiss = dismissTypographyDialog, - ) - } + if (showTypographyDialog) { + TypographyPickerDialog( + currentTypography = currentTypography, + onTypographySelected = onTypographyChange, + onDismiss = dismissTypographyDialog, + ) + } - if (showRecurrentPaymentsViewModeDialog) { - RecurrentPaymentsViewModePickerDialog( - currentMode = recurrentPaymentsViewMode, - onModeSelected = onRecurrentPaymentsViewModeChange, - onDismiss = dismissRecurrentPaymentsViewModeDialog, - ) - } + if (showRecurrentPaymentsViewModeDialog) { + RecurrentPaymentsViewModePickerDialog( + currentMode = recurrentPaymentsViewMode, + onModeSelected = onRecurrentPaymentsViewModeChange, + onDismiss = dismissRecurrentPaymentsViewModeDialog, + ) + } - if (showNotificationTimePicker) { - NotificationTimePickerDialog( - initialHour = notificationHour, - initialMinute = notificationMinute, - onDismiss = dismissNotificationTimePicker, - onTimeSelected = { hour, minute -> - onNotificationTimeChange(hour, minute) - dismissNotificationTimePicker() - }) - } + if (showNotificationTimePicker) { + NotificationTimePickerDialog( + initialHour = notificationHour, + initialMinute = notificationMinute, + onDismiss = dismissNotificationTimePicker, + onTimeSelected = { hour, minute -> + onNotificationTimeChange(hour, minute) + dismissNotificationTimePicker() + } + ) + } - if (showRecurrentNotificationTimePicker) { - NotificationTimePickerDialog( - initialHour = recurrentNotificationHour, - initialMinute = recurrentNotificationMinute, - onDismiss = dismissRecurrentNotificationTimePicker, - onTimeSelected = { hour, minute -> - onRecurrentNotificationTimeChange(hour, minute) - dismissRecurrentNotificationTimePicker() - }) - } - } + if (showRecurrentNotificationTimePicker) { + NotificationTimePickerDialog( + initialHour = recurrentNotificationHour, + initialMinute = recurrentNotificationMinute, + onDismiss = dismissRecurrentNotificationTimePicker, + onTimeSelected = { hour, minute -> + onRecurrentNotificationTimeChange(hour, minute) + dismissRecurrentNotificationTimePicker() + } + ) + } + } } @Composable fun ThemePickerDialog( - currentTheme: String, onThemeSelected: (String) -> Unit, onDismiss: () -> Unit + currentTheme: String, + onThemeSelected: (String) -> Unit, + onDismiss: () -> Unit ) { - Dialog(onDismissRequest = onDismiss) { - Surface( - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - modifier = Modifier.testTag("ThemePickerDialog") - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - Text( - text = stringResource(R.string.settings_theme_dialog_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 16.dp) - ) + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + modifier = Modifier.testTag("ThemePickerDialog") + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + Text( + text = stringResource(R.string.settings_theme_dialog_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 16.dp) + ) - ThemeOption( - title = stringResource(R.string.settings_theme_light_title), - subtitle = stringResource(R.string.settings_theme_light_subtitle), - icon = Icons.Default.LightMode, - isSelected = currentTheme == "Light", - onClick = { - onThemeSelected("Light") - onDismiss() - }) + ThemeOption( + title = stringResource(R.string.settings_theme_light_title), + subtitle = stringResource(R.string.settings_theme_light_subtitle), + icon = Icons.Default.LightMode, + isSelected = currentTheme == "Light", + onClick = { + onThemeSelected("Light") + onDismiss() + } + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - ThemeOption( - title = stringResource(R.string.settings_theme_dark_title), - subtitle = stringResource(R.string.settings_theme_dark_subtitle), - icon = Icons.Default.DarkMode, - isSelected = currentTheme == "Dark", - onClick = { - onThemeSelected("Dark") - onDismiss() - }) + ThemeOption( + title = stringResource(R.string.settings_theme_dark_title), + subtitle = stringResource(R.string.settings_theme_dark_subtitle), + icon = Icons.Default.DarkMode, + isSelected = currentTheme == "Dark", + onClick = { + onThemeSelected("Dark") + onDismiss() + } + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - ThemeOption( - title = stringResource(R.string.settings_theme_system_title), - subtitle = stringResource(R.string.settings_theme_system_subtitle), - icon = Icons.Default.Brightness4, - isSelected = currentTheme == "System", - onClick = { - onThemeSelected("System") - onDismiss() - }) - } - } - } + ThemeOption( + title = stringResource(R.string.settings_theme_system_title), + subtitle = stringResource(R.string.settings_theme_system_subtitle), + icon = Icons.Default.Brightness4, + isSelected = currentTheme == "System", + onClick = { + onThemeSelected("System") + onDismiss() + } + ) + } + } + } } @Composable private fun ThemeOption( - title: String, subtitle: String, icon: ImageVector, isSelected: Boolean, onClick: () -> Unit + title: String, + subtitle: String, + icon: ImageVector, + isSelected: Boolean, + onClick: () -> Unit ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background( - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - } - ) - .clickable(onClick = onClick) - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, contentDescription = null, tint = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, modifier = Modifier.size(24.dp) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background( + if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surface + } + ) + .clickable(onClick = onClick) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(24.dp) + ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyMediumEmphasized, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface - } - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyMediumEmphasized, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + color = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + } + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } - if (isSelected) { - RadioButton( - selected = true, onClick = null, colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary - ) - ) - } - } + if (isSelected) { + RadioButton( + selected = true, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary + ) + ) + } + } } @Composable fun TypographyPickerDialog( - currentTypography: String, - onTypographySelected: (String) -> Unit, - onDismiss: () -> Unit, + currentTypography: String, + onTypographySelected: (String) -> Unit, + onDismiss: () -> Unit, ) { - Dialog(onDismissRequest = onDismiss) { - Surface( - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - modifier = Modifier.testTag("TypographyPickerDialog") - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - Text( - text = stringResource(R.string.settings_typography_dialog_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 16.dp) - ) + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + modifier = Modifier.testTag("TypographyPickerDialog") + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + Text( + text = stringResource(R.string.settings_typography_dialog_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 16.dp) + ) - ThemeOption( - title = stringResource(R.string.settings_typography_default_title), - subtitle = stringResource(R.string.settings_typography_default_subtitle), - icon = Icons.Default.TextFields, - isSelected = currentTypography == "Default", - onClick = { - onTypographySelected("Default") - onDismiss() - } - ) + ThemeOption( + title = stringResource(R.string.settings_typography_default_title), + subtitle = stringResource(R.string.settings_typography_default_subtitle), + icon = Icons.Default.TextFields, + isSelected = currentTypography == "Default", + onClick = { + onTypographySelected("Default") + onDismiss() + } + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - ThemeOption( - title = stringResource(R.string.settings_typography_condensed_title), - subtitle = stringResource(R.string.settings_typography_condensed_subtitle), - icon = Icons.Default.TextFields, - isSelected = currentTypography == "Condensed", - onClick = { - onTypographySelected("Condensed") - onDismiss() - } - ) + ThemeOption( + title = stringResource(R.string.settings_typography_condensed_title), + subtitle = stringResource(R.string.settings_typography_condensed_subtitle), + icon = Icons.Default.TextFields, + isSelected = currentTypography == "Condensed", + onClick = { + onTypographySelected("Condensed") + onDismiss() + } + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - ThemeOption( - title = stringResource(R.string.settings_typography_expressive_title), - subtitle = stringResource(R.string.settings_typography_expressive_subtitle), - icon = Icons.Default.TextFields, - isSelected = currentTypography == "Expressive", - onClick = { - onTypographySelected("Expressive") - onDismiss() - } - ) - } - } - } + ThemeOption( + title = stringResource(R.string.settings_typography_expressive_title), + subtitle = stringResource(R.string.settings_typography_expressive_subtitle), + icon = Icons.Default.TextFields, + isSelected = currentTypography == "Expressive", + onClick = { + onTypographySelected("Expressive") + onDismiss() + } + ) + } + } + } } @Composable fun RecurrentPaymentsViewModePickerDialog( - currentMode: RecurrentPaymentsViewMode, - onModeSelected: (RecurrentPaymentsViewMode) -> Unit, - onDismiss: () -> Unit, + currentMode: RecurrentPaymentsViewMode, + onModeSelected: (RecurrentPaymentsViewMode) -> Unit, + onDismiss: () -> Unit, ) { - Dialog(onDismissRequest = onDismiss) { - Surface( - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - modifier = Modifier.testTag("RecurrentPaymentsViewModePickerDialog") - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - Text( - text = stringResource(R.string.settings_recurrent_payments_view_mode_dialog_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 16.dp) - ) + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + modifier = Modifier.testTag("RecurrentPaymentsViewModePickerDialog") + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + Text( + text = stringResource(R.string.settings_recurrent_payments_view_mode_dialog_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 16.dp) + ) - ThemeOption( - title = stringResource(R.string.settings_recurrent_payments_view_mode_horizontal_title), - subtitle = stringResource(R.string.settings_recurrent_payments_view_mode_horizontal_subtitle), - icon = Icons.Default.Repeat, - isSelected = currentMode == RecurrentPaymentsViewMode.HORIZONTAL_LIST, - onClick = { - onModeSelected(RecurrentPaymentsViewMode.HORIZONTAL_LIST) - onDismiss() - } - ) + ThemeOption( + title = stringResource(R.string.settings_recurrent_payments_view_mode_horizontal_title), + subtitle = stringResource(R.string.settings_recurrent_payments_view_mode_horizontal_subtitle), + icon = Icons.Default.Repeat, + isSelected = currentMode == RecurrentPaymentsViewMode.HORIZONTAL_LIST, + onClick = { + onModeSelected(RecurrentPaymentsViewMode.HORIZONTAL_LIST) + onDismiss() + } + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - ThemeOption( - title = stringResource(R.string.settings_recurrent_payments_view_mode_vertical_title), - subtitle = stringResource(R.string.settings_recurrent_payments_view_mode_vertical_subtitle), - icon = Icons.Default.Repeat, - isSelected = currentMode == RecurrentPaymentsViewMode.VERTICAL_LIST, - onClick = { - onModeSelected(RecurrentPaymentsViewMode.VERTICAL_LIST) - onDismiss() - } - ) - } - } - } + ThemeOption( + title = stringResource(R.string.settings_recurrent_payments_view_mode_vertical_title), + subtitle = stringResource(R.string.settings_recurrent_payments_view_mode_vertical_subtitle), + icon = Icons.Default.Repeat, + isSelected = currentMode == RecurrentPaymentsViewMode.VERTICAL_LIST, + onClick = { + onModeSelected(RecurrentPaymentsViewMode.VERTICAL_LIST) + onDismiss() + } + ) + } + } + } } @Composable private fun NotificationTimePickerDialog( - initialHour: Int, initialMinute: Int, onDismiss: () -> Unit, onTimeSelected: (Int, Int) -> Unit + initialHour: Int, + initialMinute: Int, + onDismiss: () -> Unit, + onTimeSelected: (Int, Int) -> Unit ) { - val context = LocalContext.current - DisposableEffect(context, initialHour, initialMinute) { - val dialog = TimePickerDialog( - context, - { _, hour, minute -> onTimeSelected(hour, minute) }, - initialHour, - initialMinute, - android.text.format.DateFormat.is24HourFormat(context) - ) - dialog.setOnDismissListener { onDismiss() } - dialog.show() - onDispose { - dialog.setOnDismissListener(null) - dialog.dismiss() - } - } + val context = LocalContext.current + val state = rememberTimePickerState( + initialHour = initialHour, + initialMinute = initialMinute, + is24Hour = android.text.format.DateFormat.is24HourFormat(context), + ) + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column(modifier = Modifier.padding(24.dp)) { + TimePicker(state = state) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.settings_time_picker_cancel)) + } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + onTimeSelected(state.hour, state.minute) + onDismiss() + }) { + Text(stringResource(R.string.settings_time_picker_confirm)) + } + } + } + } + } } @Composable private fun RecurrentPaymentsViewMode.label(): String { - return when (this) { - RecurrentPaymentsViewMode.HORIZONTAL_LIST -> stringResource(R.string.settings_recurrent_payments_view_mode_horizontal_title) - RecurrentPaymentsViewMode.VERTICAL_LIST -> stringResource(R.string.settings_recurrent_payments_view_mode_vertical_title) - } + return when (this) { + RecurrentPaymentsViewMode.HORIZONTAL_LIST -> stringResource( + R.string.settings_recurrent_payments_view_mode_horizontal_title + ) + RecurrentPaymentsViewMode.VERTICAL_LIST -> stringResource( + R.string.settings_recurrent_payments_view_mode_vertical_title + ) + } } private fun Context.copyAppEnvironmentMetadataToClipboard() { - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip( - ClipData.newPlainText( - "Minus app environment metadata", - buildAppEnvironmentMetadata(), - ) - ) + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip( + ClipData.newPlainText( + "Minus app environment metadata", + buildAppEnvironmentMetadata(), + ) + ) } private fun formatNotificationTime( - context: Context, hour: Int, minute: Int + context: Context, + hour: Int, + minute: Int ): String { - val pattern = if (android.text.format.DateFormat.is24HourFormat(context)) "HH:mm" else "h:mm a" - return LocalTime.of(hour, minute) - .format(DateTimeFormatter.ofPattern(pattern, Locale.getDefault())) + val pattern = if (android.text.format.DateFormat.is24HourFormat(context)) "HH:mm" else "h:mm a" + return LocalTime.of(hour, minute) + .format(DateTimeFormatter.ofPattern(pattern, Locale.getDefault())) } @PreviewLightDark @Composable private fun PreviewSettings() { - MinusTheme { - Settings( - currentTheme = "System", - currentTypography = "Expressive", - isMaterialYouEnabled = true, - isCreditQuickToggleFeatureEnabled = false, - recurrentPaymentsViewMode = RecurrentPaymentsViewMode.HORIZONTAL_LIST, - notificationHour = 9, - notificationMinute = 0, - recurrentNotificationHour = 8, - recurrentNotificationMinute = 0, - exactAlarmEnabled = true, - onThemeChange = {}, - onTypographyChange = {}, - onMaterialYouToggle = {}, - onCreditQuickToggleFeatureToggle = {}, - onRecurrentPaymentsViewModeChange = {}, - onNotificationTimeChange = { _, _ -> }, - onRecurrentNotificationTimeChange = { _, _ -> }, - onOpenExactAlarmSettings = {}, - periodMappingMode = PeriodMappingMode.ACTIVE_BUDGET, - onPeriodMappingModeChange = {}) - } -} \ No newline at end of file + MinusTheme { + Settings( + currentTheme = "System", + currentTypography = "Expressive", + isMaterialYouEnabled = true, + isCreditQuickToggleFeatureEnabled = false, + recurrentPaymentsViewMode = RecurrentPaymentsViewMode.HORIZONTAL_LIST, + notificationHour = 9, + notificationMinute = 0, + recurrentNotificationHour = 8, + recurrentNotificationMinute = 0, + exactAlarmEnabled = true, + notificationPermissionGranted = true, + onThemeChange = {}, + onTypographyChange = {}, + onMaterialYouToggle = {}, + onCreditQuickToggleFeatureToggle = {}, + onRecurrentPaymentsViewModeChange = {}, + onNotificationTimeChange = { _, _ -> }, + onRecurrentNotificationTimeChange = { _, _ -> }, + onOpenExactAlarmSettings = {}, + onOpenNotificationSettings = {}, + periodMappingMode = PeriodMappingMode.ACTIVE_BUDGET, + onPeriodMappingModeChange = {} + ) + } +} diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsScreen.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsScreen.kt index 362e5a0..95fbdee 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsScreen.kt @@ -3,10 +3,14 @@ package com.serranoie.app.minus.presentation.ui.settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.serranoie.app.minus.presentation.ui.settings.csv.CsvTransferEntryPoint import com.serranoie.app.minus.presentation.util.LocalCensorMode @@ -21,6 +25,7 @@ fun SettingsScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current val isCensored = LocalCensorMode.current val importLauncher = rememberLauncherForActivityResult( @@ -37,6 +42,18 @@ fun SettingsScreen( viewModel.setImportLauncher(importLauncher) } + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.refreshNotificationPermission() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { @@ -65,6 +82,7 @@ fun SettingsScreen( recurrentNotificationHour = uiState.recurrentNotificationHour, recurrentNotificationMinute = uiState.recurrentNotificationMinute, exactAlarmEnabled = uiState.exactAlarmEnabled, + notificationPermissionGranted = uiState.notificationPermissionGranted, onThemeChange = viewModel::onThemeChange, onTypographyChange = viewModel::onTypographyChange, onMaterialYouToggle = viewModel::onMaterialYouToggle, @@ -73,6 +91,10 @@ fun SettingsScreen( onNotificationTimeChange = viewModel::onNotificationTimeChange, onRecurrentNotificationTimeChange = viewModel::onRecurrentNotificationTimeChange, onOpenExactAlarmSettings = viewModel::onOpenExactAlarmSettings, + onOpenNotificationSettings = { + viewModel.onOpenAppSettings() + viewModel.refreshNotificationPermission() + }, periodMappingMode = uiState.periodMappingMode, onPeriodMappingModeChange = viewModel::onPeriodMappingModeChange, onExportCsv = viewModel::onExportCsv, diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsViewModel.kt index 35bea0e..a31293f 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/SettingsViewModel.kt @@ -1,11 +1,14 @@ package com.serranoie.app.minus.presentation.ui.settings +import android.Manifest import android.app.AlarmManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.datastore.preferences.core.edit import androidx.lifecycle.ViewModel @@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import logcat.logcat import javax.inject.Inject import android.provider.Settings as AndroidSettings @@ -51,6 +55,7 @@ data class SettingsUiState( val recurrentNotificationHour: Int = 8, val recurrentNotificationMinute: Int = 0, val exactAlarmEnabled: Boolean = true, + val notificationPermissionGranted: Boolean = false, val periodMappingMode: PeriodMappingMode = PeriodMappingMode.ACTIVE_BUDGET, ) @@ -77,6 +82,38 @@ class SettingsViewModel @Inject constructor( init { loadPreferences() + refreshNotificationPermission() + } + + /** + * Re-checks POST_NOTIFICATIONS (Android 13+) so the Settings row reflects + * the latest state when the user returns from the system app-info screen. + */ + fun refreshNotificationPermission() { + val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + } else { + // Pre-Tiramisu devices grant by default. + true + } + logcat("ISAAC:Settings") { "refreshNotificationPermission -> granted=$granted" } + _uiState.update { it.copy(notificationPermissionGranted = granted) } + } + + /** + * Opens the system app-info page for this package so the user can + * enable/deny POST_NOTIFICATIONS, exact alarms, etc. + */ + fun onOpenAppSettings() { + logcat("ISAAC:Settings") { "onOpenAppSettings" } + val intent = Intent(AndroidSettings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = "package:${context.packageName}".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) } fun setCsvTransferManager(manager: CsvTransferManager) { diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/components/NotificationPermissionItem.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/components/NotificationPermissionItem.kt new file mode 100644 index 0000000..0c2da21 --- /dev/null +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/settings/components/NotificationPermissionItem.kt @@ -0,0 +1,105 @@ +package com.serranoie.app.minus.presentation.ui.settings.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.serranoie.app.minus.R +import com.serranoie.app.minus.presentation.ui.theme.component.CustomPaddedListItem +import com.serranoie.app.minus.presentation.ui.theme.component.PaddedListItemPosition + +@Composable +fun NotificationPermissionItem( + granted: Boolean, + onClick: () -> Unit, + position: PaddedListItemPosition, +) { + CustomPaddedListItem( + onClick = onClick, + position = position, + borderStroke = if (!granted) { + BorderStroke(1.dp, MaterialTheme.colorScheme.error) + } else { + null + }, + ) { + Icon( + imageVector = if (granted) Icons.Default.NotificationsActive else Icons.Default.NotificationsOff, + contentDescription = null, + tint = if (granted) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.settings_notification_permission_title), + style = MaterialTheme.typography.bodyMediumEmphasized, + color = if (granted) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.error + } + ) + Text( + text = if (granted) { + stringResource(R.string.settings_notification_permission_granted_subtitle) + } else { + stringResource(R.string.settings_notification_permission_denied_subtitle) + }, + style = MaterialTheme.typography.bodySmall, + color = if (granted) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.error + } + ) + } + if (!granted) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } else { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } +} + +@Preview +@Composable +private fun PreviewNotificationPermissionItem() { + Column { + NotificationPermissionItem( + granted = true, + onClick = {}, + position = PaddedListItemPosition.First, + ) + + NotificationPermissionItem( + granted = false, + onClick = {}, + position = PaddedListItemPosition.Last, + ) + } +} diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/Type.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/Type.kt index 51da2aa..09e1bf6 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/Type.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/Type.kt @@ -12,585 +12,758 @@ import androidx.compose.ui.unit.sp import com.serranoie.app.minus.R val GoogleSansFlex = FontFamily( - Font(R.font.google_sans_flex, FontWeight.Normal), - Font(R.font.google_sans_flex, FontWeight.Medium), - Font(R.font.google_sans_flex, FontWeight.SemiBold), - Font(R.font.google_sans_flex, FontWeight.Bold) + Font(R.font.google_sans_flex, FontWeight.Normal), + Font(R.font.google_sans_flex, FontWeight.Medium), + Font(R.font.google_sans_flex, FontWeight.SemiBold), + Font(R.font.google_sans_flex, FontWeight.Bold) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplayLargeEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(155f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(155f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplayMediumEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(132f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(132f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplaySmallEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(125f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(125f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineLargeEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(800), FontVariation.width(150f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(800), + FontVariation.width(150f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineMediumEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(150f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(150f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineSmallEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(135f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(135f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleLargeEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(135f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(135f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleMediumEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(135f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(135f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleSmallEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(115f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(115f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodyLargeEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(115f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(115f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodyMediumEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(115f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(115f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodySmallEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(115f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(115f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelLargeEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(115f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(115f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelMediumEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(125f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(125f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelSmallEmphasized = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(125f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(125f) + ) + ) ) // Condensed Font Families - width below 100 for narrow appearance @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplayLargeCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(65f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(65f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplayMediumCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(75f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(75f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexDisplaySmallCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(75f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(75f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineLargeCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(800), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(800), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineMediumCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexHeadlineSmallCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleLargeCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(700), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(700), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleMediumCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexTitleSmallCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(600), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(600), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodyLargeCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(70f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(70f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodyMediumCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(80f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(80f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexBodySmallCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(85f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(85f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelLargeCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(75f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(75f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelMediumCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(65f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(65f) + ) + ) ) @OptIn(ExperimentalTextApi::class) val GoogleSansFlexLabelSmallCondensed = FontFamily( - Font( - R.font.google_sans_flex, variationSettings = FontVariation.Settings( - FontVariation.weight(500), FontVariation.width(75f) - ) - ) + Font( + R.font.google_sans_flex, + variationSettings = FontVariation.Settings( + FontVariation.weight(500), + FontVariation.width(75f) + ) + ) ) val Typography = Typography( - displayLarge = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), displayMedium = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), displaySmall = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - - headlineLarge = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Bold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), headlineMedium = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Bold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), headlineSmall = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - - titleLarge = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), titleMedium = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), titleSmall = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - - bodyLarge = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), bodyMedium = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), bodySmall = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - - labelLarge = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), labelMedium = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), labelSmall = TextStyle( - fontFamily = GoogleSansFlex, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) + displayLarge = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp + ), + displayMedium = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp + ), + displaySmall = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp + ), + + headlineLarge = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineMedium = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + headlineSmall = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ), + + titleLarge = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + titleMedium = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + titleSmall = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), + + bodyLarge = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + bodyMedium = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + bodySmall = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), + + labelLarge = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), + labelMedium = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), + labelSmall = TextStyle( + fontFamily = GoogleSansFlex, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) ) @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun Typography.withEmphasizedStyles(): Typography { - return this.copy( - displayLargeEmphasized = TextStyle( - fontFamily = GoogleSansFlexDisplayLargeEmphasized, - fontSize = 64.sp, - lineHeight = 72.sp, - letterSpacing = 0.sp - ), displayMediumEmphasized = TextStyle( - fontFamily = GoogleSansFlexDisplayMediumEmphasized, - fontSize = 52.sp, - lineHeight = 60.sp, - letterSpacing = 0.sp - ), displaySmallEmphasized = TextStyle( - fontFamily = GoogleSansFlexDisplaySmallEmphasized, - fontSize = 44.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - - // Emphasized Headline - Bold and wide for attention-grabbing headers - headlineLargeEmphasized = TextStyle( - fontFamily = GoogleSansFlexHeadlineLargeEmphasized, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), headlineMediumEmphasized = TextStyle( - fontFamily = GoogleSansFlexHeadlineMediumEmphasized, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), headlineSmallEmphasized = TextStyle( - fontFamily = GoogleSansFlexHeadlineSmallEmphasized, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), titleLargeEmphasized = TextStyle( - fontFamily = GoogleSansFlexTitleLargeEmphasized, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.15.sp - ), titleMediumEmphasized = TextStyle( - fontFamily = GoogleSansFlexTitleMediumEmphasized, - fontSize = 18.sp, - lineHeight = 26.sp, - letterSpacing = 0.2.sp - ), titleSmallEmphasized = TextStyle( - fontFamily = GoogleSansFlexTitleSmallEmphasized, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), bodyLargeEmphasized = TextStyle( - fontFamily = GoogleSansFlexBodyLargeEmphasized, - fontSize = 18.sp, - lineHeight = 28.sp, - letterSpacing = 0.6.sp - ), bodyMediumEmphasized = TextStyle( - fontFamily = GoogleSansFlexBodyMediumEmphasized, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.4.sp - ), bodySmallEmphasized = TextStyle( - fontFamily = GoogleSansFlexBodySmallEmphasized, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.5.sp - ), labelLargeEmphasized = TextStyle( - fontFamily = GoogleSansFlexLabelLargeEmphasized, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), labelMediumEmphasized = TextStyle( - fontFamily = GoogleSansFlexLabelMediumEmphasized, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.6.sp - ), labelSmallEmphasized = TextStyle( - fontFamily = GoogleSansFlexLabelSmallEmphasized, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.6.sp - ) - ) + return this.copy( + displayLargeEmphasized = TextStyle( + fontFamily = GoogleSansFlexDisplayLargeEmphasized, + fontSize = 64.sp, + lineHeight = 72.sp, + letterSpacing = 0.sp + ), + displayMediumEmphasized = TextStyle( + fontFamily = GoogleSansFlexDisplayMediumEmphasized, + fontSize = 52.sp, + lineHeight = 60.sp, + letterSpacing = 0.sp + ), + displaySmallEmphasized = TextStyle( + fontFamily = GoogleSansFlexDisplaySmallEmphasized, + fontSize = 44.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp + ), + + // Emphasized Headline - Bold and wide for attention-grabbing headers + headlineLargeEmphasized = TextStyle( + fontFamily = GoogleSansFlexHeadlineLargeEmphasized, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp + ), + headlineMediumEmphasized = TextStyle( + fontFamily = GoogleSansFlexHeadlineMediumEmphasized, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineSmallEmphasized = TextStyle( + fontFamily = GoogleSansFlexHeadlineSmallEmphasized, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + titleLargeEmphasized = TextStyle( + fontFamily = GoogleSansFlexTitleLargeEmphasized, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.15.sp + ), + titleMediumEmphasized = TextStyle( + fontFamily = GoogleSansFlexTitleMediumEmphasized, + fontSize = 18.sp, + lineHeight = 26.sp, + letterSpacing = 0.2.sp + ), + titleSmallEmphasized = TextStyle( + fontFamily = GoogleSansFlexTitleSmallEmphasized, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + bodyLargeEmphasized = TextStyle( + fontFamily = GoogleSansFlexBodyLargeEmphasized, + fontSize = 18.sp, + lineHeight = 28.sp, + letterSpacing = 0.6.sp + ), + bodyMediumEmphasized = TextStyle( + fontFamily = GoogleSansFlexBodyMediumEmphasized, + fontSize = 16.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), + bodySmallEmphasized = TextStyle( + fontFamily = GoogleSansFlexBodySmallEmphasized, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp + ), + labelLargeEmphasized = TextStyle( + fontFamily = GoogleSansFlexLabelLargeEmphasized, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + labelMediumEmphasized = TextStyle( + fontFamily = GoogleSansFlexLabelMediumEmphasized, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.6.sp + ), + labelSmallEmphasized = TextStyle( + fontFamily = GoogleSansFlexLabelSmallEmphasized, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.6.sp + ) + ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun Typography.withCondensedStyles(): Typography { - return Typography( - displayLarge = TextStyle( - fontFamily = GoogleSansFlexDisplayLargeCondensed, - fontSize = 64.sp, - lineHeight = 72.sp, - letterSpacing = 0.sp - ), - displayMedium = TextStyle( - fontFamily = GoogleSansFlexDisplayMediumCondensed, - fontSize = 52.sp, - lineHeight = 60.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontFamily = GoogleSansFlexDisplaySmallCondensed, - fontSize = 44.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - headlineLarge = TextStyle( - fontFamily = GoogleSansFlexHeadlineLargeCondensed, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontFamily = GoogleSansFlexHeadlineMediumCondensed, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontFamily = GoogleSansFlexHeadlineSmallCondensed, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - titleLarge = TextStyle( - fontFamily = GoogleSansFlexTitleLargeCondensed, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.15.sp - ), - titleMedium = TextStyle( - fontFamily = GoogleSansFlexTitleMediumCondensed, - fontSize = 18.sp, - lineHeight = 26.sp, - letterSpacing = 0.2.sp - ), - titleSmall = TextStyle( - fontFamily = GoogleSansFlexTitleSmallCondensed, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - bodyLarge = TextStyle( - fontFamily = GoogleSansFlexBodyLargeCondensed, - fontSize = 18.sp, - lineHeight = 28.sp, - letterSpacing = 0.6.sp - ), - bodyMedium = TextStyle( - fontFamily = GoogleSansFlexBodyMediumCondensed, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.4.sp - ), - bodySmall = TextStyle( - fontFamily = GoogleSansFlexBodySmallCondensed, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.5.sp - ), - labelLarge = TextStyle( - fontFamily = GoogleSansFlexLabelLargeCondensed, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - labelMedium = TextStyle( - fontFamily = GoogleSansFlexLabelMediumCondensed, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.6.sp - ), - labelSmall = TextStyle( - fontFamily = GoogleSansFlexLabelSmallCondensed, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.6.sp - ) - ) + return Typography( + displayLarge = TextStyle( + fontFamily = GoogleSansFlexDisplayLargeCondensed, + fontSize = 64.sp, + lineHeight = 72.sp, + letterSpacing = 0.sp + ), + displayMedium = TextStyle( + fontFamily = GoogleSansFlexDisplayMediumCondensed, + fontSize = 52.sp, + lineHeight = 60.sp, + letterSpacing = 0.sp + ), + displaySmall = TextStyle( + fontFamily = GoogleSansFlexDisplaySmallCondensed, + fontSize = 44.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp + ), + headlineLarge = TextStyle( + fontFamily = GoogleSansFlexHeadlineLargeCondensed, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp + ), + headlineMedium = TextStyle( + fontFamily = GoogleSansFlexHeadlineMediumCondensed, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineSmall = TextStyle( + fontFamily = GoogleSansFlexHeadlineSmallCondensed, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + titleLarge = TextStyle( + fontFamily = GoogleSansFlexTitleLargeCondensed, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.15.sp + ), + titleMedium = TextStyle( + fontFamily = GoogleSansFlexTitleMediumCondensed, + fontSize = 18.sp, + lineHeight = 26.sp, + letterSpacing = 0.2.sp + ), + titleSmall = TextStyle( + fontFamily = GoogleSansFlexTitleSmallCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + bodyLarge = TextStyle( + fontFamily = GoogleSansFlexBodyLargeCondensed, + fontSize = 18.sp, + lineHeight = 28.sp, + letterSpacing = 0.6.sp + ), + bodyMedium = TextStyle( + fontFamily = GoogleSansFlexBodyMediumCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.4.sp + ), + bodySmall = TextStyle( + fontFamily = GoogleSansFlexBodySmallCondensed, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp + ), + labelLarge = TextStyle( + fontFamily = GoogleSansFlexLabelLargeCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + labelMedium = TextStyle( + fontFamily = GoogleSansFlexLabelMediumCondensed, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.6.sp + ), + labelSmall = TextStyle( + fontFamily = GoogleSansFlexLabelSmallCondensed, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.6.sp + ) + ) } -val Typography.displayLargeCondensed: TextStyle get() = displayLarge.copy(fontFamily = GoogleSansFlexDisplayLargeCondensed, fontSize = 64.sp, lineHeight = 72.sp, letterSpacing = 0.sp) -val Typography.displayMediumCondensed: TextStyle get() = displayMedium.copy(fontFamily = GoogleSansFlexDisplayMediumCondensed, fontSize = 52.sp, lineHeight = 60.sp, letterSpacing = 0.sp) -val Typography.displaySmallCondensed: TextStyle get() = displaySmall.copy(fontFamily = GoogleSansFlexDisplaySmallCondensed, fontSize = 44.sp, lineHeight = 52.sp, letterSpacing = 0.sp) - -val Typography.headlineLargeCondensed: TextStyle get() = headlineLarge.copy(fontFamily = GoogleSansFlexHeadlineLargeCondensed, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp) -val Typography.headlineMediumCondensed: TextStyle get() = headlineMedium.copy(fontFamily = GoogleSansFlexHeadlineMediumCondensed, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp) -val Typography.headlineSmallCondensed: TextStyle get() = headlineSmall.copy(fontFamily = GoogleSansFlexHeadlineSmallCondensed, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp) - -val Typography.titleLargeCondensed: TextStyle get() = titleLarge.copy(fontFamily = GoogleSansFlexTitleLargeCondensed, fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.15.sp) -val Typography.titleMediumCondensed: TextStyle get() = titleMedium.copy(fontFamily = GoogleSansFlexTitleMediumCondensed, fontSize = 18.sp, lineHeight = 26.sp, letterSpacing = 0.2.sp) -val Typography.titleSmallCondensed: TextStyle get() = titleSmall.copy(fontFamily = GoogleSansFlexTitleSmallCondensed, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp) - -val Typography.bodyLargeCondensed: TextStyle get() = bodyLarge.copy(fontFamily = GoogleSansFlexBodyLargeCondensed, fontSize = 18.sp, lineHeight = 28.sp, letterSpacing = 0.6.sp) -val Typography.bodyMediumCondensed: TextStyle get() = bodyMedium.copy(fontFamily = GoogleSansFlexBodyMediumCondensed, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.4.sp) -val Typography.bodySmallCondensed: TextStyle get() = bodySmall.copy(fontFamily = GoogleSansFlexBodySmallCondensed, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.5.sp) - -val Typography.labelLargeCondensed: TextStyle get() = labelLarge.copy(fontFamily = GoogleSansFlexLabelLargeCondensed, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp) -val Typography.labelMediumCondensed: TextStyle get() = labelMedium.copy(fontFamily = GoogleSansFlexLabelMediumCondensed, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.6.sp) -val Typography.labelSmallCondensed: TextStyle get() = labelSmall.copy(fontFamily = GoogleSansFlexLabelSmallCondensed, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.6.sp) +val Typography.displayLargeCondensed: TextStyle + get() = displayLarge.copy( + fontFamily = GoogleSansFlexDisplayLargeCondensed, + fontSize = 64.sp, + lineHeight = 72.sp, + letterSpacing = 0.sp + ) +val Typography.displayMediumCondensed: TextStyle + get() = displayMedium.copy( + fontFamily = GoogleSansFlexDisplayMediumCondensed, + fontSize = 52.sp, + lineHeight = 60.sp, + letterSpacing = 0.sp + ) +val Typography.displaySmallCondensed: TextStyle + get() = displaySmall.copy( + fontFamily = GoogleSansFlexDisplaySmallCondensed, + fontSize = 44.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp + ) + +val Typography.headlineLargeCondensed: TextStyle + get() = headlineLarge.copy( + fontFamily = GoogleSansFlexHeadlineLargeCondensed, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp + ) +val Typography.headlineMediumCondensed: TextStyle + get() = headlineMedium.copy( + fontFamily = GoogleSansFlexHeadlineMediumCondensed, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ) +val Typography.headlineSmallCondensed: TextStyle + get() = headlineSmall.copy( + fontFamily = GoogleSansFlexHeadlineSmallCondensed, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ) + +val Typography.titleLargeCondensed: TextStyle + get() = titleLarge.copy( + fontFamily = GoogleSansFlexTitleLargeCondensed, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.15.sp + ) +val Typography.titleMediumCondensed: TextStyle + get() = titleMedium.copy( + fontFamily = GoogleSansFlexTitleMediumCondensed, + fontSize = 18.sp, + lineHeight = 26.sp, + letterSpacing = 0.2.sp + ) +val Typography.titleSmallCondensed: TextStyle + get() = titleSmall.copy( + fontFamily = GoogleSansFlexTitleSmallCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ) + +val Typography.bodyLargeCondensed: TextStyle + get() = bodyLarge.copy( + fontFamily = GoogleSansFlexBodyLargeCondensed, + fontSize = 18.sp, + lineHeight = 28.sp, + letterSpacing = 0.6.sp + ) +val Typography.bodyMediumCondensed: TextStyle + get() = bodyMedium.copy( + fontFamily = GoogleSansFlexBodyMediumCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.4.sp + ) +val Typography.bodySmallCondensed: TextStyle + get() = bodySmall.copy( + fontFamily = GoogleSansFlexBodySmallCondensed, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp + ) + +val Typography.labelLargeCondensed: TextStyle + get() = labelLarge.copy( + fontFamily = GoogleSansFlexLabelLargeCondensed, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ) +val Typography.labelMediumCondensed: TextStyle + get() = labelMedium.copy( + fontFamily = GoogleSansFlexLabelMediumCondensed, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.6.sp + ) +val Typography.labelSmallCondensed: TextStyle + get() = labelSmall.copy( + fontFamily = GoogleSansFlexLabelSmallCondensed, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.6.sp + ) @OptIn(ExperimentalMaterial3ExpressiveApi::class) val ExpressiveTypography = Typography.withEmphasizedStyles() diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/component/SettingsGroup.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/component/SettingsGroup.kt index 1b0542a..68c17cf 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/component/SettingsGroup.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/theme/component/SettingsGroup.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -61,8 +62,8 @@ fun FlexibleListGroup( ) { Column( modifier = modifier - .padding(16.dp) - .padding(vertical = 8.dp) + .padding(16.dp) + .padding(vertical = 8.dp) ) { title?.let { Text( @@ -108,9 +109,9 @@ fun ListItem( Column { Row( modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(16.dp), + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { leadingIcon?.invoke() @@ -160,9 +161,9 @@ fun CustomSettingsItem( ) { Row( modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(16.dp), + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, content = content ) @@ -234,13 +235,13 @@ fun PaddedListItem( tonalElevation = 2.dp, color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier - .fillMaxWidth() - .clip(shape) + .fillMaxWidth() + .clip(shape) ) { Row( modifier = Modifier - .clickable { onClick() } - .padding(16.dp), + .clickable { onClick() } + .padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = icon, contentDescription = null) Spacer(modifier = Modifier.width(16.dp)) @@ -276,6 +277,7 @@ fun CustomPaddedListItem( background: Color = MaterialTheme.colorScheme.surfaceContainer, contentColor: Color = MaterialTheme.colorScheme.onSurface, onLongClick: (() -> Unit)? = null, + borderStroke: BorderStroke? = null, content: @Composable RowScope.() -> Unit ) { val shape = when (position) { @@ -295,17 +297,18 @@ fun CustomPaddedListItem( shape = shape, color = background, contentColor = contentColor, + border = borderStroke, modifier = modifier - .fillMaxWidth() - .clip(shape) + .fillMaxWidth() + .clip(shape) ) { Row( modifier = Modifier - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .padding(horizontal = 16.dp, vertical = 12.dp), + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, content = content ) @@ -348,19 +351,17 @@ fun CustomPaddedExpandableItem( tonalElevation = 4.dp, color = MaterialTheme.colorScheme.surfaceContainer, modifier = modifier - .fillMaxWidth() - .clip(shape) + .fillMaxWidth() + .clip(shape) ) { Column { - // Default content - always clickable Row( modifier = Modifier - .clickable { onToggleExpanded() } - .padding(16.dp), + .clickable { onToggleExpanded() } + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, content = defaultContent) - // Expanded content - only shown when expanded AnimatedVisibility( visible = isExpanded, enter = expandVertically() + fadeIn(), @@ -374,17 +375,10 @@ fun CustomPaddedExpandableItem( } } -/** - * Enum to define the position of an item in a padded list for proper corner rounding. - */ enum class PaddedListItemPosition { First, Middle, Last, Single } -data class SettingItem( - val title: String, val subtitle: String? = null, val icon: ImageVector, val onClick: () -> Unit -) - @Preview(showBackground = true) @Composable fun FlexibleSettingsGroupPreview() { @@ -439,8 +433,8 @@ fun FlexibleSettingsGroupPreview() { // Any other composable can go here Box( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + .fillMaxWidth() + .padding(16.dp) ) { Text( text = "You can put any composable content here!", diff --git a/app/src/main/java/com/serranoie/app/minus/presentation/ui/wallet/Wallet.kt b/app/src/main/java/com/serranoie/app/minus/presentation/ui/wallet/Wallet.kt index 8ef0eed..40a0679 100644 --- a/app/src/main/java/com/serranoie/app/minus/presentation/ui/wallet/Wallet.kt +++ b/app/src/main/java/com/serranoie/app/minus/presentation/ui/wallet/Wallet.kt @@ -13,39 +13,37 @@ import com.serranoie.app.minus.presentation.ui.budget.BudgetViewModel import com.serranoie.app.minus.presentation.ui.editor.EditBudgetContent import com.serranoie.app.minus.presentation.ui.theme.component.wallet.WalletStatusBarStub -const val WALLET_SHEET = "wallet" - @Composable fun Wallet( - forceChange: Boolean = false, - activityResultRegistryOwner: ActivityResultRegistryOwner? = null, - budgetViewModel: BudgetViewModel = hiltViewModel(), - onClose: () -> Unit = {}, - onOnboardingComplete: () -> Unit = {}, + forceChange: Boolean = false, + activityResultRegistryOwner: ActivityResultRegistryOwner? = null, + budgetViewModel: BudgetViewModel = hiltViewModel(), + onClose: () -> Unit = {}, + onOnboardingComplete: () -> Unit = {}, ) { - val uiState by budgetViewModel.uiState.collectAsStateWithLifecycle() - val budgetSettings = uiState.budgetSettings + val uiState by budgetViewModel.uiState.collectAsStateWithLifecycle() + val budgetSettings = uiState.budgetSettings - Column(modifier = Modifier.fillMaxSize()) { - WalletStatusBarStub() - Surface(modifier = Modifier.fillMaxSize()) { - EditBudgetContent( - budgetSettings = budgetSettings, - title = if (budgetSettings != null) "Editar presupuesto" else "Nuevo presupuesto", - buttonLabel = if (budgetSettings != null) "Actualizar" else "Aplicar", - showPreviousValuesChip = budgetSettings != null, - onBack = onClose, - onApply = { newSettings -> - budgetViewModel.saveBudgetSettings( - newSettings, - forceNewPeriodBoundary = forceChange || budgetSettings == null - ) - if (forceChange || budgetSettings == null) { - onOnboardingComplete() - } - onClose() - }, - ) - } - } + Column(modifier = Modifier.fillMaxSize()) { + WalletStatusBarStub() + Surface(modifier = Modifier.fillMaxSize()) { + EditBudgetContent( + budgetSettings = budgetSettings, + title = if (budgetSettings != null) "Editar presupuesto" else "Nuevo presupuesto", + buttonLabel = if (budgetSettings != null) "Actualizar" else "Aplicar", + showPreviousValuesChip = budgetSettings != null, + onBack = onClose, + onApply = { newSettings -> + budgetViewModel.saveBudgetSettings( + newSettings, + forceNewPeriodBoundary = forceChange || budgetSettings == null + ) + if (forceChange || budgetSettings == null) { + onOnboardingComplete() + } + onClose() + }, + ) + } + } } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c7ef356..4b73032 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -211,9 +211,15 @@ La notificación se mostrará el día que termina el período a esta hora. Hora de pagos recurrentes La revisión diaria de pagos recurrentes se ejecuta a esta hora. + Permiso de notificaciones + Concedido — recibirás notificaciones + Denegado — toca para abrir los ajustes del sistema y permitir las notificaciones + Abrir Alarmas exactas Habilitado para intentar mostrar notificaciones a la hora seleccionada. Desactivado; Android puede retrasar la notificación + Cancelar + Aceptar Acerca de Obtenga más información sobre la aplicación y su desarrollo. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4950bd1..1bd7c09 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -215,9 +215,15 @@ La notification sera affichée le jour de fin de période à cette heure Heure des paiements récurrents La vérification quotidienne des paiements récurrents s’exécute à cette heure + Autorisation de notifications + Accordée — vous recevrez des notifications + Refusée — appuyez pour ouvrir les paramètres système et autoriser les notifications + Ouvrir Alarmes exactes Activé pour essayer d\'afficher les notifications à l\'heure sélectionnée Désactivé ; Android peut retarder la notification + Annuler + OK À propos En savoir plus sur l\'application et son développement diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc62cd3..4987c1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -242,9 +242,15 @@ The notification will be shown on the period end date at this time Recurring payments time Daily recurring payment checks run at this time + Notifications permission + Granted — you will receive notifications + Denied — tap to open system settings and allow notifications + Open Exact alarms Enabled to try showing notifications at the selected time Disabled; Android may delay the notification + Cancel + OK About Learn more about the app and its development diff --git a/app/src/test/java/com/serranoie/app/minus/presentation/ui/screenshot/SettingsScreenshotTest.kt b/app/src/test/java/com/serranoie/app/minus/presentation/ui/screenshot/SettingsScreenshotTest.kt index ef5ee3d..4939dc6 100644 --- a/app/src/test/java/com/serranoie/app/minus/presentation/ui/screenshot/SettingsScreenshotTest.kt +++ b/app/src/test/java/com/serranoie/app/minus/presentation/ui/screenshot/SettingsScreenshotTest.kt @@ -3,7 +3,6 @@ package com.serranoie.app.minus.presentation.ui.screenshot import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import com.android.ide.common.rendering.api.SessionParams @@ -111,6 +110,10 @@ class SettingsScreenshotTest { onResetTutorial = {}, onBugReportClick = {}, onBack = {}, + isCensored = false, + notificationPermissionGranted = true, + onOpenNotificationSettings = {}, + onNavigateToChangelog = {}, ) } } diff --git a/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDarkThemeWithCreditFeature.png b/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDarkThemeWithCreditFeature.png index 149bb37..60596b0 100644 Binary files a/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDarkThemeWithCreditFeature.png and b/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDarkThemeWithCreditFeature.png differ diff --git a/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDefaultState.png b/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDefaultState.png index aaaa46f..b789588 100644 Binary files a/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDefaultState.png and b/app/src/test/snapshots/images/com.serranoie.app.minus.presentation.ui.screenshot_SettingsScreenshotTest_settingsDefaultState.png differ