From 9e646217e6d2a1cc9f5a65657954bf96982a86fc Mon Sep 17 00:00:00 2001 From: kaajjo Date: Thu, 25 Jul 2024 02:50:01 +0300 Subject: [PATCH] ui: redesign settings * split categories to screens * handle `locale` `configChanges` * replace some deprecated things --- app/src/main/AndroidManifest.xml | 5 +- .../libresudoku/ui/components/Preferences.kt | 21 +- .../kaajjo/libresudoku/ui/game/GameScreen.kt | 10 +- .../kaajjo/libresudoku/ui/more/MoreScreen.kt | 4 +- .../ui/onboarding/WelcomeScreen.kt | 71 +- .../ui/settings/SettingsCategoriesScreen.kt | 216 +++++ .../libresudoku/ui/settings/SettingsScreen.kt | 838 ------------------ .../ui/settings/SettingsViewModel.kt | 205 ----- .../appearance/SettingsAppearanceScreen.kt | 468 ++++++++++ .../appearance/SettingsAppearanceViewModel.kt | 81 ++ .../assistance/SettingsAssistanceScreen.kt | 120 +++ .../assistance/SettingsAssistanceViewModel.kt | 41 + .../gameplay/SettingsGameplayScreen.kt | 138 +++ .../gameplay/SettingsGameplayViewModel.kt | 53 ++ .../language/SettingsLanguageScreen.kt | 169 ++++ .../ui/settings/other/SettingsOtherScreen.kt | 147 +++ .../settings/other/SettingsOtherViewModel.kt | 44 + app/src/main/res/values-ru-rRU/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + 19 files changed, 1532 insertions(+), 1111 deletions(-) create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsCategoriesScreen.kt delete mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsScreen.kt delete mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsViewModel.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceScreen.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceViewModel.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceScreen.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceViewModel.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayScreen.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayViewModel.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/language/SettingsLanguageScreen.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherScreen.kt create mode 100644 app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f46821fb..7f322e94 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,11 +14,12 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.LibreSudoku" - tools:targetApi="tiramisu"> + tools:targetApi="tiramisu" + > diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/components/Preferences.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/components/Preferences.kt index bba1e781..09930735 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/components/Preferences.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/components/Preferences.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon @@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.kaajjo.libresudoku.R import com.kaajjo.libresudoku.ui.theme.LibreSudokuTheme import com.kaajjo.libresudoku.ui.util.LightDarkPreview @@ -41,12 +43,13 @@ fun PreferenceRow( subtitle: String? = null, enabled: Boolean = true, action: @Composable (() -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.medium + shape: Shape = RoundedCornerShape(0.dp) ) { - val height = if (subtitle != null) 72.dp else 56.dp + val height = if (subtitle != null) 85.dp else 65.dp - val titleStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurface + val titleStyle = MaterialTheme.typography.titleLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + fontSize = 20.sp ) val subtitleTextStyle = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), @@ -61,22 +64,22 @@ fun PreferenceRow( onLongClick = onLongClick, onClick = onClick, enabled = enabled - ), + ) + .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { if (painter != null) { Icon( painter = painter, modifier = Modifier - .padding(start = 12.dp, end = 14.dp) + .padding(horizontal = 16.dp) .size(24.dp), - tint = MaterialTheme.colorScheme.secondary.copy(alpha = if (enabled) 1f else 0.6f), contentDescription = null, ) } Column( Modifier - .padding(horizontal = 16.dp) + .padding(horizontal = if (painter != null) 0.dp else 16.dp) .weight(1f), ) { Text( @@ -89,7 +92,7 @@ fun PreferenceRow( modifier = Modifier.padding(top = 4.dp), text = subtitle, style = subtitleTextStyle, - color = subtitleTextStyle.color.copy(alpha = if (enabled) 1f else 0.6f), + color = subtitleTextStyle.color.copy(alpha = 0.75f), ) } } diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt index 0a5b691c..9c18d4cd 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/game/GameScreen.kt @@ -17,8 +17,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Redo import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.rounded.Redo import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu @@ -48,7 +48,6 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -59,13 +58,14 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kaajjo.libresudoku.R import com.kaajjo.libresudoku.core.Cell import com.kaajjo.libresudoku.core.PreferencesConstants import com.kaajjo.libresudoku.core.qqwing.GameType import com.kaajjo.libresudoku.core.utils.SudokuParser -import com.kaajjo.libresudoku.destinations.SettingsScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsCategoriesScreenDestination import com.kaajjo.libresudoku.ui.components.AnimatedNavigation import com.kaajjo.libresudoku.ui.components.board.Board import com.kaajjo.libresudoku.ui.game.components.DefaultGameKeyboard @@ -206,7 +206,7 @@ fun GameScreen( viewModel.giveUpDialog = true }, onSettingsClick = { - navigator.navigate(SettingsScreenDestination(launchedFromGame = true)) + navigator.navigate(SettingsCategoriesScreenDestination(launchedFromGame = true)) viewModel.showMenu = false }, onExportClick = { @@ -604,7 +604,7 @@ fun UndoRedoMenu( text = { Text(stringResource(R.string.redo)) }, leadingIcon = { Icon( - imageVector = Icons.Rounded.Redo, + imageVector = Icons.AutoMirrored.Rounded.Redo, contentDescription = null ) }, diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/more/MoreScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/more/MoreScreen.kt index a2e46ba9..9195c7cc 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/more/MoreScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/more/MoreScreen.kt @@ -24,7 +24,7 @@ import com.kaajjo.libresudoku.destinations.AboutScreenDestination import com.kaajjo.libresudoku.destinations.BackupScreenDestination import com.kaajjo.libresudoku.destinations.FoldersScreenDestination import com.kaajjo.libresudoku.destinations.LearnScreenDestination -import com.kaajjo.libresudoku.destinations.SettingsScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsCategoriesScreenDestination import com.kaajjo.libresudoku.ui.components.AnimatedNavigation import com.kaajjo.libresudoku.ui.components.PreferenceRow import com.ramcosta.composedestinations.annotation.Destination @@ -61,7 +61,7 @@ fun MoreScreen( PreferenceRow( title = stringResource(R.string.settings_title), painter = painterResource(R.drawable.ic_settings_24), - onClick = { navigator.navigate(SettingsScreenDestination()) } + onClick = { navigator.navigate(SettingsCategoriesScreenDestination()) } ) PreferenceRow( title = stringResource(R.string.backup_restore_title), diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/onboarding/WelcomeScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/onboarding/WelcomeScreen.kt index 78f0bf0d..6c5105b4 100644 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/onboarding/WelcomeScreen.kt +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/onboarding/WelcomeScreen.kt @@ -1,6 +1,5 @@ package com.kaajjo.libresudoku.ui.onboarding -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -29,11 +28,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,7 +46,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.os.LocaleListCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -60,12 +56,10 @@ import com.kaajjo.libresudoku.core.utils.SudokuParser import com.kaajjo.libresudoku.data.datastore.AppSettingsManager import com.kaajjo.libresudoku.destinations.BackupScreenDestination import com.kaajjo.libresudoku.destinations.HomeScreenDestination -import com.kaajjo.libresudoku.destinations.SettingsScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsCategoriesScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsLanguageScreenDestination import com.kaajjo.libresudoku.ui.components.board.Board -import com.kaajjo.libresudoku.ui.settings.SelectionDialog import com.kaajjo.libresudoku.ui.util.getCurrentLocaleString -import com.kaajjo.libresudoku.ui.util.getCurrentLocaleTag -import com.kaajjo.libresudoku.ui.util.getLangs import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import dagger.hilt.android.lifecycle.HiltViewModel @@ -79,11 +73,14 @@ fun WelcomeScreen( viewModel: WelcomeViewModel = hiltViewModel(), navigator: DestinationsNavigator ) { + val context = LocalContext.current + val currentLanguage by remember { + mutableStateOf( + getCurrentLocaleString(context) + ) + } + Scaffold { paddingValues -> - val context = LocalContext.current - var languagePickDialog by rememberSaveable { - mutableStateOf(false) - } Column( modifier = Modifier .fillMaxSize() @@ -128,20 +125,12 @@ fun WelcomeScreen( ) { Text(stringResource(R.string.action_start)) } - var currentLanguage by remember { - mutableStateOf( - getCurrentLocaleString(context) - ) - } - LaunchedEffect(languagePickDialog) { - currentLanguage = getCurrentLocaleString(context) - } ItemRowBigIcon( title = stringResource(R.string.pref_app_language), icon = Icons.Rounded.Language, subtitle = currentLanguage, - onClick = { languagePickDialog = true }, + onClick = { navigator.navigate(SettingsLanguageScreenDestination()) }, ) ItemRowBigIcon( title = stringResource(R.string.onboard_restore_backup), @@ -156,30 +145,12 @@ fun WelcomeScreen( icon = Icons.Rounded.Settings, subtitle = stringResource(R.string.onboard_settings_description), onClick = { - navigator.navigate(SettingsScreenDestination(false)) + navigator.navigate(SettingsCategoriesScreenDestination(false)) } ) } } } - - if (languagePickDialog) { - SelectionDialog( - title = stringResource(R.string.pref_app_language), - entries = getLangs(context), - selected = getCurrentLocaleTag(), - onSelect = { localeKey -> - val locale = if (localeKey == "") { - LocaleListCompat.getEmptyLocaleList() - } else { - LocaleListCompat.forLanguageTags(localeKey) - } - AppCompatDelegate.setApplicationLocales(locale) - languagePickDialog = false - }, - onDismiss = { languagePickDialog = false } - ) - } } } @@ -259,18 +230,18 @@ class WelcomeViewModel ) : ViewModel() { var selectedCell by mutableStateOf(Cell(-1, -1, 0)) - // all heart shaped + // all heart shaped ❤ val previewBoard = SudokuParser().parseBoard( board = listOf( - "072000350340502018100030009800000003030000070050000020008000600000103000760050041", - "017000230920608054400010009200000001060000020040000090002000800000503000390020047", - "052000180480906023600020007500000008020000060030000090005000300000708000370060014", - "025000860360208017700010003600000002040000090030000070006000100000507000490030058", - "049000380280309056600050007300000002010000030070000090003000800000604000420080013", - "071000420490802073300060009200000007060000090010000080007000900000703000130090068", - "023000190150402086800050004700000008090000030080000010008000700000306000530070029", - "097000280280706013300080007600000002040000060030000090001000400000105000860040051", - "049000180160904023700010004200000008090000060080000050005000600000706000470020031" + "072000350340502018100030009800000003030000070050000020008000600000103000760050041", + "017000230920608054400010009200000001060000020040000090002000800000503000390020047", + "052000180480906023600020007500000008020000060030000090005000300000708000370060014", + "025000860360208017700010003600000002040000090030000070006000100000507000490030058", + "049000380280309056600050007300000002010000030070000090003000800000604000420080013", + "071000420490802073300060009200000007060000090010000080007000900000703000130090068", + "023000190150402086800050004700000008090000030080000010008000700000306000530070029", + "097000280280706013300080007600000002040000060030000090001000400000105000860040051", + "049000180160904023700010004200000008090000060080000050005000600000706000470020031" ).random(), gameType = GameType.Default9x9 ) diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsCategoriesScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsCategoriesScreen.kt new file mode 100644 index 00000000..8cd22914 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsCategoriesScreen.kt @@ -0,0 +1,216 @@ +package com.kaajjo.libresudoku.ui.settings + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Extension +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material.icons.outlined.MoreHoriz +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material.icons.outlined.TipsAndUpdates +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.destinations.SettingsAppearanceScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsAssistanceScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsGameplayScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsLanguageScreenDestination +import com.kaajjo.libresudoku.destinations.SettingsOtherScreenDestination +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.CollapsingTitle +import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.CollapsingTopAppBar +import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.rememberTopAppBarScrollBehavior +import com.kaajjo.libresudoku.ui.settings.components.AppThemePreviewItem +import com.kaajjo.libresudoku.ui.util.getCurrentLocaleString +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@Destination(style = AnimatedNavigation::class) +@Composable +fun SettingsCategoriesScreen( + navigator: DestinationsNavigator, + launchedFromGame: Boolean = false +) { + val context = LocalContext.current + val currentLanguage by remember { mutableStateOf(getCurrentLocaleString(context)) } + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.settings_title), + navigator = navigator + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + PreferenceRow( + title = stringResource(R.string.pref_appearance), + subtitle = stringResource(R.string.perf_appearance_summary), + onClick = { + navigator.navigate(SettingsAppearanceScreenDestination()) + }, + painter = rememberVectorPainter(Icons.Outlined.Palette) + ) + } + + item { + PreferenceRow( + title = stringResource(R.string.pref_gameplay), + subtitle = stringResource(R.string.perf_gameplay_summary), + onClick = { + navigator.navigate(SettingsGameplayScreenDestination()) + }, + painter = rememberVectorPainter(Icons.Outlined.Extension) + ) + } + + item { + PreferenceRow( + title = stringResource(R.string.pref_assistance), + subtitle = stringResource(R.string.perf_assistance_summary), + onClick = { + navigator.navigate(SettingsAssistanceScreenDestination()) + }, + painter = rememberVectorPainter(Icons.Outlined.TipsAndUpdates) + ) + } + + item { + PreferenceRow( + title = stringResource(R.string.pref_app_language), + subtitle = currentLanguage, + onClick = { + navigator.navigate(SettingsLanguageScreenDestination()) + }, + painter = rememberVectorPainter(Icons.Outlined.Language) + ) + } + item { + PreferenceRow( + title = stringResource(R.string.pref_other), + subtitle = stringResource(R.string.perf_other_summary), + onClick = { + navigator.navigate(SettingsOtherScreenDestination(launchedFromGame = launchedFromGame)) + }, + painter = rememberVectorPainter(Icons.Outlined.MoreHoriz) + ) + } + } + } +} + +@Composable +fun SettingsCategory( + modifier: Modifier = Modifier, + title: String +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, bottom = 16.dp, top = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +fun AppThemeItem( + title: String, + colorScheme: ColorScheme, + amoledBlack: Boolean, + darkTheme: Int, + selected: Boolean, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .width(115.dp) + .padding(start = 8.dp, end = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + AppThemePreviewItem( + selected = selected, + onClick = onClick, + colorScheme = colorScheme.copy( + background = + if (amoledBlack && (darkTheme == 0 && isSystemInDarkTheme() || darkTheme == 2)) { + Color.Black + } else { + colorScheme.background + } + ), + shapes = MaterialTheme.shapes + ) + Text( + text = title, + style = MaterialTheme.typography.labelSmall + ) + } +} + +@Composable +fun SettingsScaffoldLazyColumn( + navigator: DestinationsNavigator, + titleText: String, + snackbarHostState: SnackbarHostState? = null, + content: @Composable (PaddingValues) -> Unit +) { + val scrollBehavior = rememberTopAppBarScrollBehavior() + + Scaffold( + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { + snackbarHostState?.let { + SnackbarHost(it) + } + }, + topBar = { + CollapsingTopAppBar( + collapsingTitle = CollapsingTitle.medium(titleText = titleText), + navigationIcon = { + IconButton(onClick = { navigator.popBackStack() }) { + Icon( + painter = painterResource(R.drawable.ic_round_arrow_back_24), + contentDescription = null + ) + } + }, + scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + content(paddingValues) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsScreen.kt deleted file mode 100644 index 418ea247..00000000 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsScreen.kt +++ /dev/null @@ -1,838 +0,0 @@ -package com.kaajjo.libresudoku.ui.settings - -import android.os.Build -import android.widget.Toast -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp -import androidx.core.os.LocaleListCompat -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.kaajjo.libresudoku.R -import com.kaajjo.libresudoku.core.PreferencesConstants -import com.kaajjo.libresudoku.data.datastore.AppSettingsManager -import com.kaajjo.libresudoku.data.datastore.ThemeSettingsManager -import com.kaajjo.libresudoku.destinations.SettingsBoardThemeDestination -import com.kaajjo.libresudoku.ui.components.AnimatedNavigation -import com.kaajjo.libresudoku.ui.components.PreferenceRow -import com.kaajjo.libresudoku.ui.components.PreferenceRowSwitch -import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn -import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.CollapsingTitle -import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.CollapsingTopAppBar -import com.kaajjo.libresudoku.ui.components.collapsing_topappbar.rememberTopAppBarScrollBehavior -import com.kaajjo.libresudoku.ui.settings.components.AppThemePreviewItem -import com.kaajjo.libresudoku.ui.settings.components.ColorPickerDialog -import com.kaajjo.libresudoku.ui.theme.LibreSudokuTheme -import com.kaajjo.libresudoku.ui.util.getCurrentLocaleString -import com.kaajjo.libresudoku.ui.util.getCurrentLocaleTag -import com.kaajjo.libresudoku.ui.util.getLangs -import com.materialkolor.PaletteStyle -import com.materialkolor.rememberDynamicColorScheme -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlinx.coroutines.launch -import java.time.ZonedDateTime -import java.time.chrono.IsoChronology -import java.time.format.DateTimeFormatter -import java.time.format.DateTimeFormatterBuilder -import java.time.format.FormatStyle -import java.util.Locale - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class) -@Destination(style = AnimatedNavigation::class) -@Composable -fun SettingsScreen( - viewModel: SettingsViewModel = hiltViewModel(), - navigator: DestinationsNavigator, - launchedFromGame: Boolean = false -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val scrollBehavior = rememberTopAppBarScrollBehavior() - val snackbarHostState = remember { SnackbarHostState() } - - val highlightMistakes by viewModel.highlightMistakes.collectAsStateWithLifecycle( - initialValue = PreferencesConstants.DEFAULT_HIGHLIGHT_MISTAKES - ) - val inputMethod by viewModel.inputMethod.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_INPUT_METHOD) - val darkTheme by viewModel.darkTheme.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_DARK_THEME) - val dateFormat by viewModel.dateFormat.collectAsStateWithLifecycle(initialValue = "") - val dynamicColors by viewModel.dynamicColors.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_DYNAMIC_COLORS) - val amoledBlackState by viewModel.amoledBlack.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_AMOLED_BLACK) - val hintDisabled by viewModel.disableHints.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_HINTS_DISABLED) - val mistakesLimit by viewModel.mistakesLimit.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_MISTAKES_LIMIT) - val timerEnabled by viewModel.timer.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_SHOW_TIMER) - val resetTimer by viewModel.canResetTimer.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_GAME_RESET_TIMER) - val keepScreenOn by viewModel.keepScreenOn.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_KEEP_SCREEN_ON) - val autoEraseNotes by viewModel.autoEraseNotes.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_AUTO_ERASE_NOTES) - val highlightIdentical by viewModel.highlightIdentical.collectAsStateWithLifecycle( - initialValue = PreferencesConstants.DEFAULT_HIGHLIGHT_IDENTICAL - ) - val remainingUse by viewModel.remainingUse.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_REMAINING_USES) - val currentPaletteStyle by viewModel.paletteStyle.collectAsStateWithLifecycle(initialValue = PaletteStyle.TonalSpot) - val currentSeedColor by viewModel.seedColor.collectAsStateWithLifecycle( - initialValue = Color( - PreferencesConstants.DEFAULT_THEME_SEED_COLOR - ) - ) - val isUserDefinedSeedColor by viewModel.isUserDefinedSeedColor.collectAsStateWithLifecycle( - initialValue = false - ) - - var paletteStyleDialog by rememberSaveable { - mutableStateOf(false) - } - var colorPickerDialog by rememberSaveable { - mutableStateOf(false) - } - - Scaffold( - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - snackbarHost = { SnackbarHost(snackbarHostState) }, - topBar = { - CollapsingTopAppBar( - collapsingTitle = CollapsingTitle.medium(titleText = stringResource(R.string.settings_title)), - navigationIcon = { - IconButton(onClick = { navigator.popBackStack() }) { - Icon( - painter = painterResource(R.drawable.ic_round_arrow_back_24), - contentDescription = null - ) - } - }, - scrollBehavior = scrollBehavior - ) - } - ) { paddingValues -> - ScrollbarLazyColumn( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth() - ) { - item { - SettingsCategory( - title = stringResource(R.string.pref_appearance) - ) - PreferenceRow( - title = stringResource(R.string.pref_dark_theme), - subtitle = when (darkTheme) { - 0 -> stringResource(R.string.pref_dark_theme_follow) - 1 -> stringResource(R.string.pref_dark_theme_off) - 2 -> stringResource(R.string.pref_dark_theme_on) - else -> "" - }, - onClick = { viewModel.darkModeDialog = true } - ) - } - - item { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.pref_app_theme) - ) - LazyRow( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - item { - LibreSudokuTheme( - dynamicColor = true, - darkTheme = when (darkTheme) { - 0 -> isSystemInDarkTheme() - 1 -> false - else -> true - }, - amoled = amoledBlackState - ) { - Column( - modifier = Modifier - .width(115.dp) - .padding(start = 8.dp, end = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - AppThemePreviewItem( - selected = dynamicColors, - onClick = { - viewModel.updateDynamicColors(true) - viewModel.updateIsUserDefinedSeedColor(false) - }, - colorScheme = MaterialTheme.colorScheme, - shapes = MaterialTheme.shapes - ) - Text( - text = stringResource(R.string.theme_dynamic), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - } - items( - listOf( - Color.Green to context.getString(R.string.theme_green), - Color.Red to context.getString(R.string.theme_peach), - Color.Yellow to context.getString(R.string.theme_yellow), - Color.Blue to context.getString(R.string.theme_blue), - Color(0xFFC97820) to context.getString(R.string.theme_orange), - Color.Cyan to context.getString(R.string.theme_cyan), - Color.Magenta to context.getString(R.string.theme_lavender) - ) - ) { - AppThemeItem( - title = it.second, - colorScheme = rememberDynamicColorScheme( - seedColor = it.first, - isDark = when (darkTheme) { - 0 -> isSystemInDarkTheme() - 1 -> false - else -> true - }, - style = currentPaletteStyle - ), - onClick = { - viewModel.updateDynamicColors(false) - viewModel.updateCurrentSeedColor(it.first) - viewModel.updateIsUserDefinedSeedColor(false) - }, - selected = currentSeedColor == it.first && !dynamicColors && !isUserDefinedSeedColor, - amoledBlack = amoledBlackState, - darkTheme = darkTheme, - ) - } - - item { - Box { - AppThemeItem( - title = stringResource(R.string.theme_custom), - colorScheme = rememberDynamicColorScheme( - seedColor = currentSeedColor, - isDark = when (darkTheme) { - 0 -> isSystemInDarkTheme() - 1 -> false - else -> true - }, - style = currentPaletteStyle - ), - onClick = { - viewModel.updateDynamicColors(false) - viewModel.updateIsUserDefinedSeedColor(true) - colorPickerDialog = true - }, - selected = isUserDefinedSeedColor, - amoledBlack = amoledBlackState, - darkTheme = darkTheme, - ) - Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = null, - modifier = Modifier - .padding(top = 8.dp, end = 16.dp) - .align(Alignment.TopEnd) - .size(24.dp) - ) - } - } - } - } - item { - PreferenceRow( - title = stringResource(R.string.pref_monet_style), - subtitle = when (currentPaletteStyle) { - PaletteStyle.TonalSpot -> stringResource(R.string.monet_tonalspot) - PaletteStyle.Neutral -> stringResource(R.string.monet_neutral) - PaletteStyle.Vibrant -> stringResource(R.string.monet_vibrant) - PaletteStyle.Expressive -> stringResource(R.string.monet_expressive) - PaletteStyle.Rainbow -> stringResource(R.string.monet_rainbow) - PaletteStyle.FruitSalad -> stringResource(R.string.monet_fruitsalad) - PaletteStyle.Monochrome -> stringResource(R.string.monet_monochrome) - PaletteStyle.Fidelity -> stringResource(R.string.monet_fidelity) - PaletteStyle.Content -> stringResource(R.string.monet_content) - }, - onClick = { paletteStyleDialog = true } - ) - } - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_pure_black), - checked = amoledBlackState, - onClick = { - viewModel.updateAmoledBlack(!amoledBlackState) - } - ) - } - - item { - PreferenceRow( - title = stringResource(R.string.pref_board_theme_title), - subtitle = stringResource(R.string.pref_board_theme_subtitle), - onClick = { navigator.navigate(SettingsBoardThemeDestination()) } - ) - } - - item { - var currentLanguage by remember { - mutableStateOf( - getCurrentLocaleString(context) - ) - } - LaunchedEffect(viewModel.languagePickDialog) { - currentLanguage = getCurrentLocaleString(context) - } - PreferenceRow( - title = stringResource(R.string.pref_app_language), - subtitle = currentLanguage, - onClick = { viewModel.languagePickDialog = true } - ) - } - - item { - PreferenceRow( - title = stringResource(R.string.pref_date_format), - subtitle = "${dateFormat.ifEmpty { stringResource(R.string.label_default) }} (${ - ZonedDateTime.now().format(AppSettingsManager.dateFormat(dateFormat)) - })", - onClick = { viewModel.dateFormatDialog = true } - ) - } - - item { - HorizontalDivider( - modifier = Modifier.fillMaxWidth() - ) - - SettingsCategory( - title = stringResource(R.string.pref_gameplay) - ) - - PreferenceRow( - title = stringResource(R.string.pref_input), - subtitle = when (inputMethod) { - 0 -> stringResource(R.string.pref_input_cell_first) - 1 -> stringResource(R.string.pref_input_digit_first) - else -> "" - }, - onClick = { viewModel.inputMethodDialog = true } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_mistakes_limit), - subtitle = stringResource(R.string.pref_mistakes_limit_summ), - checked = mistakesLimit, - onClick = { viewModel.updateMistakesLimit(!mistakesLimit) } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_disable_hints), - subtitle = stringResource(R.string.pref_disable_hints_summ), - checked = hintDisabled, - onClick = { viewModel.updateHintDisabled(!hintDisabled) } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_show_timer), - checked = timerEnabled, - onClick = { viewModel.updateTimer(!timerEnabled) } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_reset_timer), - checked = resetTimer, - onClick = { viewModel.updateCanResetTimer(!resetTimer) } - ) - } - - item { - val funKeyboardOverNum by viewModel.funKeyboardOverNum.collectAsStateWithLifecycle( - initialValue = PreferencesConstants.DEFAULT_FUN_KEYBOARD_OVER_NUM - ) - PreferenceRowSwitch( - title = stringResource(R.string.pref_fun_keyboard_over_num), - subtitle = stringResource(R.string.pref_fun_keyboard_over_num_subtitle), - checked = funKeyboardOverNum, - onClick = { - viewModel.updateFunKeyboardOverNum(!funKeyboardOverNum) - } - ) - } - - item { - HorizontalDivider( - modifier = Modifier.fillMaxWidth() - ) - - SettingsCategory( - title = stringResource(R.string.pref_assistance) - ) - - PreferenceRow( - title = stringResource(R.string.pref_mistakes_check), - subtitle = when (highlightMistakes) { - 0 -> stringResource(R.string.pref_mistakes_check_off) - 1 -> stringResource(R.string.pref_mistakes_check_violations) - 2 -> stringResource(R.string.pref_mistakes_check_final) - else -> stringResource(R.string.pref_mistakes_check_off) - }, - onClick = { viewModel.mistakesDialog = true } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_highlight_identical), - subtitle = stringResource(R.string.pref_highlight_identical_summ), - checked = highlightIdentical, - onClick = { - viewModel.updateHighlightIdentical(!highlightIdentical) - } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_remaining_uses), - subtitle = stringResource(R.string.pref_remaining_uses_summ), - checked = remainingUse, - onClick = { viewModel.updateRemainingUse(!remainingUse) } - ) - - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_auto_erase_notes), - checked = autoEraseNotes, - onClick = { viewModel.updateAutoEraseNotes(!autoEraseNotes) } - ) - } - - - item { - HorizontalDivider( - modifier = Modifier.fillMaxWidth() - ) - - SettingsCategory( - title = stringResource(R.string.pref_other) - ) - val saveLastSelectedDifficultyType by viewModel.saveLastSelectedDifficultyType - .collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_SAVE_LAST_SELECTED_DIFF_TYPE) - PreferenceRowSwitch( - title = stringResource(R.string.pref_save_last_diff_and_type), - subtitle = stringResource(R.string.pref_save_last_diff_and_type_subtitle), - checked = saveLastSelectedDifficultyType, - onClick = { - viewModel.updateSaveLastSelectedDifficultyType(!saveLastSelectedDifficultyType) - } - ) - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_keep_screen_on), - checked = keepScreenOn, - onClick = { - viewModel.updateKeepScreenOn(!keepScreenOn) - } - ) - } - - item { - PreferenceRow( - title = stringResource(R.string.pref_reset_tipcards), - onClick = { - viewModel.resetTipCards() - scope.launch { - snackbarHostState.showSnackbar( - context.resources.getString(R.string.pref_tipcards_reset) - ) - } - } - ) - } - - if (!launchedFromGame) { - item { - PreferenceRow( - title = stringResource(R.string.pref_delete_stats), - onClick = { - viewModel.resetStatsDialog = true - } - ) - } - } - - item { - PreferenceRowSwitch( - title = stringResource(R.string.pref_crash_reporting), - subtitle = stringResource(R.string.pref_crash_reporting_subtitle), - checked = viewModel.crashReportingEnabled, - onClick = { - viewModel.updateCrashReportingEnabled(!viewModel.crashReportingEnabled) - } - ) - } - } - - if (viewModel.mistakesDialog) { - SelectionDialog( - title = stringResource(R.string.pref_mistakes_check), - selections = listOf( - stringResource(R.string.pref_mistakes_check_off), - stringResource(R.string.pref_mistakes_check_violations), - stringResource(R.string.pref_mistakes_check_final) - ), - selected = highlightMistakes, - onSelect = { index -> - viewModel.updateMistakesHighlight(index) - }, - onDismiss = { viewModel.mistakesDialog = false } - ) - } else if (viewModel.darkModeDialog) { - SelectionDialog( - title = stringResource(R.string.pref_dark_theme), - selections = listOf( - stringResource(R.string.pref_dark_theme_follow), - stringResource(R.string.pref_dark_theme_off), - stringResource(R.string.pref_dark_theme_on) - ), - selected = darkTheme, - onSelect = { index -> - viewModel.updateDarkTheme(index) - }, - onDismiss = { viewModel.darkModeDialog = false } - ) - } else if (viewModel.inputMethodDialog) { - SelectionDialog( - title = stringResource(R.string.pref_input), - selections = listOf( - stringResource(R.string.pref_input_cell_first), - stringResource(R.string.pref_input_digit_first) - ), - selected = inputMethod, - onSelect = { index -> - viewModel.updateInputMethod(index) - }, - onDismiss = { viewModel.inputMethodDialog = false } - ) - } else if (viewModel.resetStatsDialog) { - AlertDialog( - title = { Text(stringResource(R.string.pref_delete_stats)) }, - text = { Text(stringResource(R.string.pref_delete_stats_summ)) }, - confirmButton = { - TextButton(onClick = { - viewModel.deleteAllTables() - viewModel.resetStatsDialog = false - scope.launch { - snackbarHostState.showSnackbar( - context.resources.getString(R.string.action_deleted) - ) - } - }) { - Text( - text = stringResource(R.string.action_delete), - color = MaterialTheme.colorScheme.error - ) - } - }, - dismissButton = { - FilledTonalButton(onClick = { viewModel.resetStatsDialog = false }) { - Text(stringResource(R.string.action_cancel)) - } - }, - onDismissRequest = { viewModel.resetStatsDialog = false } - ) - } else if (viewModel.languagePickDialog) { - SelectionDialog( - title = stringResource(R.string.pref_app_language), - entries = getLangs(context), - selected = getCurrentLocaleTag(), - onSelect = { localeKey -> - val locale = if (localeKey == "") { - LocaleListCompat.getEmptyLocaleList() - } else { - LocaleListCompat.forLanguageTags(localeKey) - } - AppCompatDelegate.setApplicationLocales(locale) - viewModel.languagePickDialog = false - }, - onDismiss = { viewModel.languagePickDialog = false } - ) - } else if (viewModel.dateFormatDialog) { - DateFormatDialog( - title = stringResource(R.string.pref_date_format), - entries = DateFormats.associateWith { dateFormatEntry -> - val dateString = ZonedDateTime.now().format( - when (dateFormatEntry) { - "" -> { - DateTimeFormatter.ofPattern( - DateTimeFormatterBuilder.getLocalizedDateTimePattern( - FormatStyle.SHORT, - null, - IsoChronology.INSTANCE, - Locale.getDefault() - ) - ) - } - - else -> { - DateTimeFormatter.ofPattern(dateFormatEntry) - } - } - ) - "${dateFormatEntry.ifEmpty { stringResource(R.string.label_default) }} ($dateString)" - }, - customDateFormatText = - if (!DateFormats.contains(dateFormat)) - "$dateFormat (${ - ZonedDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat)) - })" - else stringResource(R.string.pref_date_format_custom_label), - selected = dateFormat, - onSelect = { format -> - if (format == "custom") { - viewModel.customFormatDialog = true - } else { - viewModel.updateDateFormat(format) - } - viewModel.dateFormatDialog = false - }, - onDismiss = { viewModel.dateFormatDialog = false }, - - ) - } else if (paletteStyleDialog) { - SelectionDialog( - title = stringResource(R.string.pref_monet_style), - selections = listOf( - stringResource(R.string.monet_tonalspot), - stringResource(R.string.monet_neutral), - stringResource(R.string.monet_vibrant), - stringResource(R.string.monet_expressive), - stringResource(R.string.monet_rainbow), - stringResource(R.string.monet_fruitsalad), - stringResource(R.string.monet_monochrome), - stringResource(R.string.monet_fidelity), - stringResource(R.string.monet_content) - ), - selected = ThemeSettingsManager.paletteStyles.find { it.first == currentPaletteStyle }?.second - ?: 0, - onSelect = { index -> - viewModel.updatePaletteStyle(index) - }, - onDismiss = { paletteStyleDialog = false } - ) - } else if (colorPickerDialog) { - val clipboardManager = LocalClipboardManager.current - var currentColor by remember { - mutableStateOf(currentSeedColor.toArgb()) - } - ColorPickerDialog( - currentColor = currentColor, - onConfirm = { - viewModel.updateCurrentSeedColor(Color(currentColor)) - colorPickerDialog = false - }, - onDismiss = { - colorPickerDialog = false - }, - onHexColorClick = { - clipboardManager.setText( - AnnotatedString( - "#" + currentColor.toHexString( - HexFormat.UpperCase - ) - ) - ) - }, - onRandomColorClick = { - currentColor = (Math.random() * 16777215).toInt() or (0xFF shl 24) - }, - onColorChange = { - currentColor = it - }, - onPaste = { - val clipboardContent = clipboardManager.getText() - var parsedColor: Int? = null - if (clipboardContent != null) { - try { - parsedColor = android.graphics.Color.parseColor( - clipboardContent.text - ) - } catch (_: Exception) { - - } - } - if (parsedColor != null) { - currentColor = parsedColor - } else { - Toast - .makeText( - context, - context.getString(R.string.parse_color_fail), - Toast.LENGTH_SHORT - ) - .show() - } - } - ) - } - - if (viewModel.customFormatDialog) { - var customDateFormat by rememberSaveable { - mutableStateOf( - if (DateFormats.contains( - dateFormat - ) - ) "" else dateFormat - ) - } - var invalidCustomDateFormat by rememberSaveable { mutableStateOf(false) } - var dateFormatPreview by rememberSaveable { mutableStateOf("") } - - SetDateFormatPatternDialog( - onConfirm = { - if (viewModel.checkCustomDateFormat(customDateFormat)) { - viewModel.updateDateFormat(customDateFormat) - invalidCustomDateFormat = false - viewModel.customFormatDialog = false - } else { - invalidCustomDateFormat = true - } - }, - onDismissRequest = { viewModel.customFormatDialog = false }, - onTextValueChange = { text -> - customDateFormat = text - if (invalidCustomDateFormat) invalidCustomDateFormat = false - - dateFormatPreview = if (viewModel.checkCustomDateFormat(customDateFormat)) { - ZonedDateTime.now() - .format(DateTimeFormatter.ofPattern(customDateFormat)) - } else { - "" - } - }, - customDateFormat = customDateFormat, - invalidCustomDateFormat = invalidCustomDateFormat, - datePreview = dateFormatPreview - ) - } - } -} - -private val DateFormats = listOf( - "", - "dd/MM/yy", - "dd.MM.yy", - "MM/dd/yy", - "yyyy-MM-dd", - "dd MMM yyyy", - "MMM dd, yyyy" -) - -@Composable -fun SettingsCategory( - modifier: Modifier = Modifier, - title: String -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(start = 16.dp, bottom = 16.dp, top = 16.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - ) - } -} - -@Composable -fun AppThemeItem( - title: String, - colorScheme: ColorScheme, - amoledBlack: Boolean, - darkTheme: Int, - selected: Boolean, - onClick: () -> Unit, -) { - Column( - modifier = Modifier - .width(115.dp) - .padding(start = 8.dp, end = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - AppThemePreviewItem( - selected = selected, - onClick = onClick, - colorScheme = colorScheme.copy( - background = - if (amoledBlack && (darkTheme == 0 && isSystemInDarkTheme() || darkTheme == 2)) { - Color.Black - } else { - colorScheme.background - } - ), - shapes = MaterialTheme.shapes - ) - Text( - text = title, - style = MaterialTheme.typography.labelSmall - ) - } -} - - diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsViewModel.kt deleted file mode 100644 index a5fb4435..00000000 --- a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/SettingsViewModel.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.kaajjo.libresudoku.ui.settings - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.Color -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.kaajjo.libresudoku.data.database.AppDatabase -import com.kaajjo.libresudoku.data.datastore.AcraSharedPrefs -import com.kaajjo.libresudoku.data.datastore.AppSettingsManager -import com.kaajjo.libresudoku.data.datastore.ThemeSettingsManager -import com.kaajjo.libresudoku.data.datastore.TipCardsDataStore -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.time.format.DateTimeFormatter -import javax.inject.Inject - -@HiltViewModel -class SettingsViewModel -@Inject constructor( - private val settingsDataManager: AppSettingsManager, - private val tipCardsDataStore: TipCardsDataStore, - private val appDatabase: AppDatabase, - private val acraSharedPrefs: AcraSharedPrefs, - savedStateHandle: SavedStateHandle -) : ViewModel() { - @Inject - lateinit var appThemeDataStore: ThemeSettingsManager - - val launchedFromGame by mutableStateOf(savedStateHandle.get("fromGame")) - var resetStatsDialog by mutableStateOf(false) - - var darkModeDialog by mutableStateOf(false) - var inputMethodDialog by mutableStateOf(false) - var mistakesDialog by mutableStateOf(false) - var languagePickDialog by mutableStateOf(false) - var dateFormatDialog by mutableStateOf(false) - var customFormatDialog by mutableStateOf(false) - - var crashReportingEnabled by mutableStateOf(acraSharedPrefs.getAcraEnabled()) - - val darkTheme by lazy { - appThemeDataStore.darkTheme - } - - fun updateDarkTheme(value: Int) = - viewModelScope.launch(Dispatchers.IO) { - appThemeDataStore.setDarkTheme(value) - } - - val dynamicColors by lazy { - appThemeDataStore.dynamicColors - } - - fun updateDynamicColors(enabled: Boolean) = - viewModelScope.launch { - appThemeDataStore.setDynamicColors(enabled) - } - - val amoledBlack by lazy { - appThemeDataStore.amoledBlack - } - - fun updateAmoledBlack(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - appThemeDataStore.setAmoledBlack(enabled) - } - - - val mistakesLimit = settingsDataManager.mistakesLimit - fun updateMistakesLimit(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setMistakesLimit(enabled) - } - - val timer = settingsDataManager.timerEnabled - fun updateTimer(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setTimer(enabled) - } - - val canResetTimer = settingsDataManager.resetTimerEnabled - fun updateCanResetTimer(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setResetTimer(enabled) - } - - val highlightIdentical = settingsDataManager.highlightIdentical - fun updateHighlightIdentical(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setSameValuesHighlight(enabled) - } - - val disableHints = settingsDataManager.hintsDisabled - fun updateHintDisabled(disabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setHintsDisabled(disabled) - } - } - - val remainingUse = settingsDataManager.remainingUse - fun updateRemainingUse(enabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setRemainingUse(enabled) - } - } - - val autoEraseNotes = settingsDataManager.autoEraseNotes - fun updateAutoEraseNotes(enabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setAutoEraseNotes(enabled) - } - } - - val highlightMistakes = settingsDataManager.highlightMistakes - fun updateMistakesHighlight(index: Int) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setHighlightMistakes(index) - } - } - - val inputMethod = settingsDataManager.inputMethod - fun updateInputMethod(value: Int) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setInputMethod(value) - } - } - - fun resetTipCards() { - viewModelScope.launch { - tipCardsDataStore.setStreakCard(true) - tipCardsDataStore.setRecordCard(true) - } - } - - fun deleteAllTables() { - viewModelScope.launch(Dispatchers.IO) { - appDatabase.clearAllTables() - } - } - - val seedColor by lazy { appThemeDataStore.themeColorSeed } - fun updateCurrentSeedColor(seedColor: Color) { - viewModelScope.launch(Dispatchers.IO) { - appThemeDataStore.setCurrentThemeColor(seedColor) - } - } - - val keepScreenOn = settingsDataManager.keepScreenOn - fun updateKeepScreenOn(enabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setKeepScreenOn(enabled) - } - } - - fun updateCrashReportingEnabled(enabled: Boolean) { - acraSharedPrefs.setAcraEnabled(enabled) - crashReportingEnabled = acraSharedPrefs.getAcraEnabled() - } - - val funKeyboardOverNum = settingsDataManager.funKeyboardOverNumbers - fun updateFunKeyboardOverNum(enabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setFunKeyboardOverNum(enabled) - } - } - - val dateFormat = settingsDataManager.dateFormat - fun updateDateFormat(format: String) { - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setDateFormat(format) - } - } - - val saveLastSelectedDifficultyType = settingsDataManager.saveSelectedGameDifficultyType - fun updateSaveLastSelectedDifficultyType(enabled: Boolean) = - viewModelScope.launch(Dispatchers.IO) { - settingsDataManager.setSaveSelectedGameDifficultyType(enabled) - } - - fun checkCustomDateFormat(pattern: String): Boolean { - return try { - DateTimeFormatter.ofPattern(pattern) - true - } catch (e: Exception) { - false - } - } - - val paletteStyle by lazy { appThemeDataStore.themePaletteStyle } - fun updatePaletteStyle(index: Int) = - viewModelScope.launch(Dispatchers.IO) { - appThemeDataStore.setPaletteStyle(ThemeSettingsManager.paletteStyles[index].first) - } - - val isUserDefinedSeedColor by lazy { appThemeDataStore.isUserDefinedSeedColor } - fun updateIsUserDefinedSeedColor(value: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - appThemeDataStore.setIsUserDefinedSeedColor(value) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceScreen.kt new file mode 100644 index 00000000..bfa02390 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceScreen.kt @@ -0,0 +1,468 @@ +package com.kaajjo.libresudoku.ui.settings.appearance + +import android.os.Build +import android.widget.Toast +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Contrast +import androidx.compose.material.icons.outlined.DarkMode +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.EditCalendar +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material.icons.outlined.Tag +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.data.datastore.AppSettingsManager +import com.kaajjo.libresudoku.data.datastore.ThemeSettingsManager +import com.kaajjo.libresudoku.destinations.SettingsBoardThemeDestination +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.PreferenceRowSwitch +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.settings.AppThemeItem +import com.kaajjo.libresudoku.ui.settings.DateFormatDialog +import com.kaajjo.libresudoku.ui.settings.SelectionDialog +import com.kaajjo.libresudoku.ui.settings.SetDateFormatPatternDialog +import com.kaajjo.libresudoku.ui.settings.SettingsScaffoldLazyColumn +import com.kaajjo.libresudoku.ui.settings.components.AppThemePreviewItem +import com.kaajjo.libresudoku.ui.settings.components.ColorPickerDialog +import com.kaajjo.libresudoku.ui.theme.LibreSudokuTheme +import com.materialkolor.PaletteStyle +import com.materialkolor.rememberDynamicColorScheme +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import java.time.ZonedDateTime +import java.time.chrono.IsoChronology +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.format.FormatStyle +import java.util.Locale + +@Destination(style = AnimatedNavigation::class) +@OptIn(ExperimentalStdlibApi::class) +@Composable +fun SettingsAppearanceScreen( + viewModel: SettingsAppearanceViewModel = hiltViewModel(), + navigator: DestinationsNavigator +) { + val context = LocalContext.current + + var darkModeDialog by rememberSaveable { mutableStateOf(false) } + var dateFormatDialog by rememberSaveable { mutableStateOf(false) } + var customFormatDialog by rememberSaveable { mutableStateOf(false) } + var paletteStyleDialog by rememberSaveable { mutableStateOf(false) } + var colorPickerDialog by rememberSaveable { mutableStateOf(false) } + + val darkTheme by viewModel.darkTheme.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_DARK_THEME) + val dateFormat by viewModel.dateFormat.collectAsStateWithLifecycle(initialValue = "") + val dynamicColors by viewModel.dynamicColors.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_DYNAMIC_COLORS) + val amoledBlack by viewModel.amoledBlack.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_AMOLED_BLACK) + + val currentPaletteStyle by viewModel.paletteStyle.collectAsStateWithLifecycle(initialValue = PaletteStyle.TonalSpot) + val currentSeedColor by viewModel.seedColor.collectAsStateWithLifecycle( + initialValue = Color( + PreferencesConstants.DEFAULT_THEME_SEED_COLOR + ) + ) + val isUserDefinedSeedColor by viewModel.isUserDefinedSeedColor.collectAsStateWithLifecycle( + initialValue = false + ) + + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.pref_appearance), + navigator = navigator + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + PreferenceRow( + title = stringResource(R.string.pref_dark_theme), + subtitle = when (darkTheme) { + 0 -> stringResource(R.string.pref_dark_theme_follow) + 1 -> stringResource(R.string.pref_dark_theme_off) + 2 -> stringResource(R.string.pref_dark_theme_on) + else -> "" + }, + onClick = { darkModeDialog = true }, + painter = rememberVectorPainter(Icons.Outlined.DarkMode) + ) + } + + item { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.pref_app_theme) + ) + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp) + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + item { + LibreSudokuTheme( + dynamicColor = true, + darkTheme = when (darkTheme) { + 0 -> isSystemInDarkTheme() + 1 -> false + else -> true + }, + amoled = amoledBlack + ) { + Column( + modifier = Modifier + .width(115.dp) + .padding(start = 8.dp, end = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppThemePreviewItem( + selected = dynamicColors, + onClick = { + viewModel.updateDynamicColors(true) + viewModel.updateIsUserDefinedSeedColor(false) + }, + colorScheme = MaterialTheme.colorScheme, + shapes = MaterialTheme.shapes + ) + Text( + text = stringResource(R.string.theme_dynamic), + style = MaterialTheme.typography.labelSmall + ) + } + } + } + } + items( + listOf( + Color.Green to context.getString(R.string.theme_green), + Color.Red to context.getString(R.string.theme_peach), + Color.Yellow to context.getString(R.string.theme_yellow), + Color.Blue to context.getString(R.string.theme_blue), + Color(0xFFC97820) to context.getString(R.string.theme_orange), + Color.Cyan to context.getString(R.string.theme_cyan), + Color.Magenta to context.getString(R.string.theme_lavender) + ) + ) { + AppThemeItem( + title = it.second, + colorScheme = rememberDynamicColorScheme( + seedColor = it.first, + isDark = when (darkTheme) { + 0 -> isSystemInDarkTheme() + 1 -> false + else -> true + }, + style = currentPaletteStyle + ), + onClick = { + viewModel.updateDynamicColors(false) + viewModel.updateCurrentSeedColor(it.first) + viewModel.updateIsUserDefinedSeedColor(false) + }, + selected = currentSeedColor == it.first && !dynamicColors && !isUserDefinedSeedColor, + amoledBlack = amoledBlack, + darkTheme = darkTheme, + ) + } + + item { + Box { + AppThemeItem( + title = stringResource(R.string.theme_custom), + colorScheme = rememberDynamicColorScheme( + seedColor = currentSeedColor, + isDark = when (darkTheme) { + 0 -> isSystemInDarkTheme() + 1 -> false + else -> true + }, + style = currentPaletteStyle + ), + onClick = { + viewModel.updateDynamicColors(false) + viewModel.updateIsUserDefinedSeedColor(true) + colorPickerDialog = true + }, + selected = isUserDefinedSeedColor, + amoledBlack = amoledBlack, + darkTheme = darkTheme, + ) + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = null, + modifier = Modifier + .padding(top = 8.dp, end = 16.dp) + .align(Alignment.TopEnd) + .size(24.dp) + ) + } + } + } + } + item { + PreferenceRow( + title = stringResource(R.string.pref_monet_style), + subtitle = when (currentPaletteStyle) { + PaletteStyle.TonalSpot -> stringResource(R.string.monet_tonalspot) + PaletteStyle.Neutral -> stringResource(R.string.monet_neutral) + PaletteStyle.Vibrant -> stringResource(R.string.monet_vibrant) + PaletteStyle.Expressive -> stringResource(R.string.monet_expressive) + PaletteStyle.Rainbow -> stringResource(R.string.monet_rainbow) + PaletteStyle.FruitSalad -> stringResource(R.string.monet_fruitsalad) + PaletteStyle.Monochrome -> stringResource(R.string.monet_monochrome) + PaletteStyle.Fidelity -> stringResource(R.string.monet_fidelity) + PaletteStyle.Content -> stringResource(R.string.monet_content) + }, + onClick = { paletteStyleDialog = true }, + painter = rememberVectorPainter(Icons.Outlined.Palette) + ) + } + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_pure_black), + checked = amoledBlack, + onClick = { + viewModel.updateAmoledBlack(!amoledBlack) + }, + painter = rememberVectorPainter(Icons.Outlined.Contrast) + ) + } + item { + PreferenceRow( + title = stringResource(R.string.pref_board_theme_title), + subtitle = stringResource(R.string.pref_board_theme_summary), + onClick = { + navigator.navigate(SettingsBoardThemeDestination()) + }, + painter = rememberVectorPainter(Icons.Outlined.Tag) + ) + } + item { + PreferenceRow( + title = stringResource(R.string.pref_date_format), + subtitle = "${dateFormat.ifEmpty { stringResource(R.string.label_default) }} (${ + ZonedDateTime.now().format(AppSettingsManager.dateFormat(dateFormat)) + })", + onClick = { dateFormatDialog = true }, + painter = rememberVectorPainter(Icons.Outlined.EditCalendar) + ) + } + } + } + + if (darkModeDialog) { + SelectionDialog( + title = stringResource(R.string.pref_dark_theme), + selections = listOf( + stringResource(R.string.pref_dark_theme_follow), + stringResource(R.string.pref_dark_theme_off), + stringResource(R.string.pref_dark_theme_on) + ), + selected = darkTheme, + onSelect = { index -> + viewModel.updateDarkTheme(index) + }, + onDismiss = { darkModeDialog = false } + ) + } else if (dateFormatDialog) { + DateFormatDialog( + title = stringResource(R.string.pref_date_format), + entries = DateFormats.associateWith { dateFormatEntry -> + val dateString = ZonedDateTime.now().format( + when (dateFormatEntry) { + "" -> { + DateTimeFormatter.ofPattern( + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + FormatStyle.SHORT, + null, + IsoChronology.INSTANCE, + Locale.getDefault() + ) + ) + } + + else -> { + DateTimeFormatter.ofPattern(dateFormatEntry) + } + } + ) + "${dateFormatEntry.ifEmpty { stringResource(R.string.label_default) }} ($dateString)" + }, + customDateFormatText = + if (!DateFormats.contains(dateFormat)) + "$dateFormat (${ + ZonedDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat)) + })" + else stringResource(R.string.pref_date_format_custom_label), + selected = dateFormat, + onSelect = { format -> + if (format == "custom") { + customFormatDialog = true + } else { + viewModel.updateDateFormat(format) + } + dateFormatDialog = false + }, + onDismiss = { dateFormatDialog = false }, + + ) + } else if (paletteStyleDialog) { + SelectionDialog( + title = stringResource(R.string.pref_monet_style), + selections = listOf( + stringResource(R.string.monet_tonalspot), + stringResource(R.string.monet_neutral), + stringResource(R.string.monet_vibrant), + stringResource(R.string.monet_expressive), + stringResource(R.string.monet_rainbow), + stringResource(R.string.monet_fruitsalad), + stringResource(R.string.monet_monochrome), + stringResource(R.string.monet_fidelity), + stringResource(R.string.monet_content) + ), + selected = ThemeSettingsManager.paletteStyles.find { it.first == currentPaletteStyle }?.second + ?: 0, + onSelect = { index -> + viewModel.updatePaletteStyle(index) + }, + onDismiss = { paletteStyleDialog = false } + ) + } else if (colorPickerDialog) { + val clipboardManager = LocalClipboardManager.current + var currentColor by remember { + mutableIntStateOf(currentSeedColor.toArgb()) + } + ColorPickerDialog( + currentColor = currentColor, + onConfirm = { + viewModel.updateCurrentSeedColor(Color(currentColor)) + colorPickerDialog = false + }, + onDismiss = { + colorPickerDialog = false + }, + onHexColorClick = { + clipboardManager.setText( + AnnotatedString( + "#" + currentColor.toHexString( + HexFormat.UpperCase + ) + ) + ) + }, + onRandomColorClick = { + currentColor = (Math.random() * 16777215).toInt() or (0xFF shl 24) + }, + onColorChange = { + currentColor = it + }, + onPaste = { + val clipboardContent = clipboardManager.getText() + var parsedColor: Int? = null + if (clipboardContent != null) { + try { + parsedColor = android.graphics.Color.parseColor( + clipboardContent.text + ) + } catch (_: Exception) { + + } + } + if (parsedColor != null) { + currentColor = parsedColor + } else { + Toast + .makeText( + context, + context.getString(R.string.parse_color_fail), + Toast.LENGTH_SHORT + ) + .show() + } + } + ) + } + + if (customFormatDialog) { + var customDateFormat by rememberSaveable { + mutableStateOf( + if (DateFormats.contains( + dateFormat + ) + ) "" else dateFormat + ) + } + var invalidCustomDateFormat by rememberSaveable { mutableStateOf(false) } + var dateFormatPreview by rememberSaveable { mutableStateOf("") } + + SetDateFormatPatternDialog( + onConfirm = { + if (viewModel.checkCustomDateFormat(customDateFormat)) { + viewModel.updateDateFormat(customDateFormat) + invalidCustomDateFormat = false + customFormatDialog = false + } else { + invalidCustomDateFormat = true + } + }, + onDismissRequest = { customFormatDialog = false }, + onTextValueChange = { text -> + customDateFormat = text + if (invalidCustomDateFormat) invalidCustomDateFormat = false + + dateFormatPreview = if (viewModel.checkCustomDateFormat(customDateFormat)) { + ZonedDateTime.now() + .format(DateTimeFormatter.ofPattern(customDateFormat)) + } else { + "" + } + }, + customDateFormat = customDateFormat, + invalidCustomDateFormat = invalidCustomDateFormat, + datePreview = dateFormatPreview + ) + } +} + +private val DateFormats = listOf( + "", + "dd/MM/yy", + "dd.MM.yy", + "MM/dd/yy", + "yyyy-MM-dd", + "dd MMM yyyy", + "MMM dd, yyyy" +) \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceViewModel.kt new file mode 100644 index 00000000..ebbabb6f --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/appearance/SettingsAppearanceViewModel.kt @@ -0,0 +1,81 @@ +package com.kaajjo.libresudoku.ui.settings.appearance + +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kaajjo.libresudoku.data.datastore.AppSettingsManager +import com.kaajjo.libresudoku.data.datastore.ThemeSettingsManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +@HiltViewModel +class SettingsAppearanceViewModel @Inject constructor( + private val themeSettings: ThemeSettingsManager, + private val settings: AppSettingsManager +) : ViewModel() { + val darkTheme by lazy { + themeSettings.darkTheme + } + + fun updateDarkTheme(value: Int) = + viewModelScope.launch(Dispatchers.IO) { + themeSettings.setDarkTheme(value) + } + + val dynamicColors by lazy { + themeSettings.dynamicColors + } + + fun updateDynamicColors(enabled: Boolean) = + viewModelScope.launch { + themeSettings.setDynamicColors(enabled) + } + + val amoledBlack by lazy { + themeSettings.amoledBlack + } + + fun updateAmoledBlack(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + themeSettings.setAmoledBlack(enabled) + } + + val dateFormat = settings.dateFormat + fun updateDateFormat(format: String) { + viewModelScope.launch(Dispatchers.IO) { + settings.setDateFormat(format) + } + } + + val paletteStyle by lazy { themeSettings.themePaletteStyle } + fun updatePaletteStyle(index: Int) = + viewModelScope.launch(Dispatchers.IO) { + themeSettings.setPaletteStyle(ThemeSettingsManager.paletteStyles[index].first) + } + + val isUserDefinedSeedColor by lazy { themeSettings.isUserDefinedSeedColor } + fun updateIsUserDefinedSeedColor(value: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + themeSettings.setIsUserDefinedSeedColor(value) + } + } + + val seedColor by lazy { themeSettings.themeColorSeed } + fun updateCurrentSeedColor(seedColor: Color) { + viewModelScope.launch(Dispatchers.IO) { + themeSettings.setCurrentThemeColor(seedColor) + } + } + + fun checkCustomDateFormat(pattern: String): Boolean { + return try { + DateTimeFormatter.ofPattern(pattern) + true + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceScreen.kt new file mode 100644 index 00000000..29d1a30a --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceScreen.kt @@ -0,0 +1,120 @@ +package com.kaajjo.libresudoku.ui.settings.assistance + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Adjust +import androidx.compose.material.icons.outlined.AutoFixHigh +import androidx.compose.material.icons.outlined.LooksOne +import androidx.compose.material.icons.outlined.Pin +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.PreferenceRowSwitch +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.settings.SelectionDialog +import com.kaajjo.libresudoku.ui.settings.SettingsScaffoldLazyColumn +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@Destination(style = AnimatedNavigation::class) +@Composable +fun SettingsAssistanceScreen( + viewModel: SettingsAssistanceViewModel = hiltViewModel(), + navigator: DestinationsNavigator +) { + var mistakesDialog by rememberSaveable { mutableStateOf(false) } + + val highlightMistakes by viewModel.highlightMistakes.collectAsStateWithLifecycle( + initialValue = PreferencesConstants.DEFAULT_HIGHLIGHT_MISTAKES + ) + val autoEraseNotes by viewModel.autoEraseNotes.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_AUTO_ERASE_NOTES) + val highlightIdentical by viewModel.highlightIdentical.collectAsStateWithLifecycle( + initialValue = PreferencesConstants.DEFAULT_HIGHLIGHT_IDENTICAL + ) + val remainingUse by viewModel.remainingUse.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_REMAINING_USES) + + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.pref_assistance), + navigator = navigator + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + PreferenceRow( + title = stringResource(R.string.pref_mistakes_check), + subtitle = when (highlightMistakes) { + 0 -> stringResource(R.string.pref_mistakes_check_off) + 1 -> stringResource(R.string.pref_mistakes_check_violations) + 2 -> stringResource(R.string.pref_mistakes_check_final) + else -> stringResource(R.string.pref_mistakes_check_off) + }, + onClick = { mistakesDialog = true }, + painter = rememberVectorPainter(Icons.Outlined.Adjust) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_highlight_identical), + subtitle = stringResource(R.string.pref_highlight_identical_summ), + checked = highlightIdentical, + onClick = { + viewModel.updateHighlightIdentical(!highlightIdentical) + }, + painter = rememberVectorPainter(Icons.Outlined.LooksOne) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_remaining_uses), + subtitle = stringResource(R.string.pref_remaining_uses_summ), + checked = remainingUse, + onClick = { viewModel.updateRemainingUse(!remainingUse) }, + painter = rememberVectorPainter(Icons.Outlined.Pin) + ) + + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_auto_erase_notes), + checked = autoEraseNotes, + onClick = { viewModel.updateAutoEraseNotes(!autoEraseNotes) }, + painter = rememberVectorPainter(Icons.Outlined.AutoFixHigh) + ) + } + } + + if (mistakesDialog) { + SelectionDialog( + title = stringResource(R.string.pref_mistakes_check), + selections = listOf( + stringResource(R.string.pref_mistakes_check_off), + stringResource(R.string.pref_mistakes_check_violations), + stringResource(R.string.pref_mistakes_check_final) + ), + selected = highlightMistakes, + onSelect = { index -> + viewModel.updateMistakesHighlight(index) + }, + onDismiss = { mistakesDialog = false } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceViewModel.kt new file mode 100644 index 00000000..34c585e4 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/assistance/SettingsAssistanceViewModel.kt @@ -0,0 +1,41 @@ +package com.kaajjo.libresudoku.ui.settings.assistance + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kaajjo.libresudoku.data.datastore.AppSettingsManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsAssistanceViewModel @Inject constructor( + private val settings: AppSettingsManager, +) : ViewModel() { + val remainingUse = settings.remainingUse + fun updateRemainingUse(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settings.setRemainingUse(enabled) + } + } + + val highlightIdentical = settings.highlightIdentical + fun updateHighlightIdentical(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + settings.setSameValuesHighlight(enabled) + } + + val autoEraseNotes = settings.autoEraseNotes + fun updateAutoEraseNotes(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settings.setAutoEraseNotes(enabled) + } + } + + val highlightMistakes = settings.highlightMistakes + fun updateMistakesHighlight(index: Int) { + viewModelScope.launch(Dispatchers.IO) { + settings.setHighlightMistakes(index) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayScreen.kt new file mode 100644 index 00000000..278fd7c8 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayScreen.kt @@ -0,0 +1,138 @@ +package com.kaajjo.libresudoku.ui.settings.gameplay + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.EditNote +import androidx.compose.material.icons.outlined.HistoryToggleOff +import androidx.compose.material.icons.outlined.Schedule +import androidx.compose.material.icons.outlined.SwitchAccessShortcut +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.PreferenceRowSwitch +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.settings.SelectionDialog +import com.kaajjo.libresudoku.ui.settings.SettingsScaffoldLazyColumn +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@Destination(style = AnimatedNavigation::class) +@Composable +fun SettingsGameplayScreen( + viewModel: SettingsGameplayViewModel = hiltViewModel(), + navigator: DestinationsNavigator +) { + var inputMethodDialog by rememberSaveable { mutableStateOf(false) } + + val inputMethod by viewModel.inputMethod.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_INPUT_METHOD) + val mistakesLimit by viewModel.mistakesLimit.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_MISTAKES_LIMIT) + val hintDisabled by viewModel.disableHints.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_HINTS_DISABLED) + val timerEnabled by viewModel.timer.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_SHOW_TIMER) + val resetTimer by viewModel.canResetTimer.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_GAME_RESET_TIMER) + val funKeyboardOverNum by viewModel.funKeyboardOverNum.collectAsStateWithLifecycle( + initialValue = PreferencesConstants.DEFAULT_FUN_KEYBOARD_OVER_NUM + ) + + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.pref_gameplay), + navigator = navigator + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + PreferenceRow( + title = stringResource(R.string.pref_input), + subtitle = when (inputMethod) { + 0 -> stringResource(R.string.pref_input_cell_first) + 1 -> stringResource(R.string.pref_input_digit_first) + else -> "" + }, + onClick = { inputMethodDialog = true }, + painter = rememberVectorPainter(Icons.Outlined.EditNote) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_mistakes_limit), + subtitle = stringResource(R.string.pref_mistakes_limit_summ), + checked = mistakesLimit, + onClick = { viewModel.updateMistakesLimit(!mistakesLimit) }, + painter = rememberVectorPainter(Icons.Outlined.Block) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_disable_hints), + subtitle = stringResource(R.string.pref_disable_hints_summ), + checked = hintDisabled, + onClick = { viewModel.updateHintDisabled(!hintDisabled) }, + painter = rememberVectorPainter(Icons.Outlined.VisibilityOff) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_show_timer), + checked = timerEnabled, + onClick = { viewModel.updateTimer(!timerEnabled) }, + painter = rememberVectorPainter(Icons.Outlined.Schedule) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_reset_timer), + checked = resetTimer, + onClick = { viewModel.updateCanResetTimer(!resetTimer) }, + painter = rememberVectorPainter(Icons.Outlined.HistoryToggleOff) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_fun_keyboard_over_num), + subtitle = stringResource(R.string.pref_fun_keyboard_over_num_subtitle), + checked = funKeyboardOverNum, + onClick = { + viewModel.updateFunKeyboardOverNum(!funKeyboardOverNum) + }, + painter = rememberVectorPainter(Icons.Outlined.SwitchAccessShortcut) + ) + } + } + + if (inputMethodDialog) { + SelectionDialog( + title = stringResource(R.string.pref_input), + selections = listOf( + stringResource(R.string.pref_input_cell_first), + stringResource(R.string.pref_input_digit_first) + ), + selected = inputMethod, + onSelect = { index -> + viewModel.updateInputMethod(index) + }, + onDismiss = { inputMethodDialog = false } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayViewModel.kt new file mode 100644 index 00000000..2195138a --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/gameplay/SettingsGameplayViewModel.kt @@ -0,0 +1,53 @@ +package com.kaajjo.libresudoku.ui.settings.gameplay + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kaajjo.libresudoku.data.datastore.AppSettingsManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsGameplayViewModel @Inject constructor( + private val settings: AppSettingsManager +) : ViewModel() { + val inputMethod = settings.inputMethod + fun updateInputMethod(value: Int) { + viewModelScope.launch(Dispatchers.IO) { + settings.setInputMethod(value) + } + } + + val mistakesLimit = settings.mistakesLimit + fun updateMistakesLimit(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + settings.setMistakesLimit(enabled) + } + + val timer = settings.timerEnabled + fun updateTimer(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + settings.setTimer(enabled) + } + + val canResetTimer = settings.resetTimerEnabled + fun updateCanResetTimer(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + settings.setResetTimer(enabled) + } + + val disableHints = settings.hintsDisabled + fun updateHintDisabled(disabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settings.setHintsDisabled(disabled) + } + } + + val funKeyboardOverNum = settings.funKeyboardOverNumbers + fun updateFunKeyboardOverNum(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settings.setFunKeyboardOverNum(enabled) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/language/SettingsLanguageScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/language/SettingsLanguageScreen.kt new file mode 100644 index 00000000..0dd913d9 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/language/SettingsLanguageScreen.kt @@ -0,0 +1,169 @@ +package com.kaajjo.libresudoku.ui.settings.language + +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.os.LocaleListCompat +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.core.WEBLATE_ENGAGE +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.settings.SettingsScaffoldLazyColumn +import com.kaajjo.libresudoku.ui.util.findActivity +import com.kaajjo.libresudoku.ui.util.getCurrentLocaleTag +import com.kaajjo.libresudoku.ui.util.getLangs +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@Destination(style = AnimatedNavigation::class) +@Composable +fun SettingsLanguageScreen( + navigator: DestinationsNavigator +) { + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val appLanguages by remember { mutableStateOf(getLangs(context)) } + var currentLanguage by remember { mutableStateOf(getCurrentLocaleTag()) } + + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.pref_app_language), + navigator = navigator + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + HelpTranslateCard( + onClick = { + uriHandler.openUri(WEBLATE_ENGAGE) + } + ) + } + item { + Spacer(modifier = Modifier.height(12.dp)) + } + items(appLanguages.toList()) { language -> + LanguageItem( + languageName = language.second, + selected = currentLanguage == language.first, + onClick = { + val locale = if (language.first == "") { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(language.first) + } + AppCompatDelegate.setApplicationLocales(locale) + currentLanguage = getCurrentLocaleTag() + + // react to locale change only on android < 13 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + context.findActivity()?.recreate() + } + } + ) + } + } + } +} + +@Composable +private fun HelpTranslateCard( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .clip(MaterialTheme.shapes.extraLarge) + .background(MaterialTheme.colorScheme.secondaryContainer) + .clickable(onClick = onClick) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_weblate), + contentDescription = null + ) + Column( + modifier = Modifier + .padding(start = 12.dp) + .weight(1f) + ) { + Text( + text = stringResource(R.string.help_translate), + style = MaterialTheme.typography.titleLarge.copy(fontSize = 24.sp ) + ) + Text( + text = stringResource(R.string.hosted_weblate) + ) + } + Icon( + imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = null + ) + } + } +} + +@Composable +private fun LanguageItem( + languageName: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + PreferenceRow( + modifier = modifier.background( + if (selected) { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.surface + } + ), + title = languageName, + onClick = onClick, + action = { + RadioButton( + selected = selected, + onClick = onClick + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherScreen.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherScreen.kt new file mode 100644 index 00000000..215c606a --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherScreen.kt @@ -0,0 +1,147 @@ +package com.kaajjo.libresudoku.ui.settings.other + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Bookmark +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Smartphone +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kaajjo.libresudoku.R +import com.kaajjo.libresudoku.core.PreferencesConstants +import com.kaajjo.libresudoku.ui.components.AnimatedNavigation +import com.kaajjo.libresudoku.ui.components.PreferenceRow +import com.kaajjo.libresudoku.ui.components.PreferenceRowSwitch +import com.kaajjo.libresudoku.ui.components.ScrollbarLazyColumn +import com.kaajjo.libresudoku.ui.settings.SettingsScaffoldLazyColumn +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.launch + +@Destination(style = AnimatedNavigation::class) +@Composable +fun SettingsOtherScreen( + viewModel: SettingsOtherViewModel = hiltViewModel(), + navigator: DestinationsNavigator, + launchedFromGame: Boolean = false +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + var resetGameDataDialog by rememberSaveable { mutableStateOf(false) } + + val saveLastSelectedDifficultyType by viewModel.saveLastSelectedDifficultyType + .collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_SAVE_LAST_SELECTED_DIFF_TYPE) + val keepScreenOn by viewModel.keepScreenOn.collectAsStateWithLifecycle(initialValue = PreferencesConstants.DEFAULT_KEEP_SCREEN_ON) + + SettingsScaffoldLazyColumn( + titleText = stringResource(R.string.pref_other), + navigator = navigator, + snackbarHostState = snackbarHostState + ) { paddingValues -> + ScrollbarLazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_save_last_diff_and_type), + subtitle = stringResource(R.string.pref_save_last_diff_and_type_subtitle), + checked = saveLastSelectedDifficultyType, + onClick = { + viewModel.updateSaveLastSelectedDifficultyType(!saveLastSelectedDifficultyType) + }, + painter = rememberVectorPainter(Icons.Outlined.Bookmark) + ) + } + + item { + PreferenceRowSwitch( + title = stringResource(R.string.pref_keep_screen_on), + checked = keepScreenOn, + onClick = { + viewModel.updateKeepScreenOn(!keepScreenOn) + }, + painter = rememberVectorPainter(Icons.Outlined.Smartphone) + ) + } + + item { + PreferenceRow( + title = stringResource(R.string.pref_reset_tipcards), + onClick = { + viewModel.resetTipCards() + scope.launch { + snackbarHostState.showSnackbar( + context.resources.getString(R.string.pref_tipcards_reset) + ) + } + }, + painter = rememberVectorPainter(Icons.Outlined.Clear) + ) + } + + if (!launchedFromGame) { + item { + PreferenceRow( + title = stringResource(R.string.pref_delete_stats), + onClick = { + resetGameDataDialog = true + }, + painter = rememberVectorPainter(Icons.Outlined.Delete) + ) + } + } + } + + if (resetGameDataDialog) { + AlertDialog( + title = { Text(stringResource(R.string.pref_delete_stats)) }, + text = { Text(stringResource(R.string.pref_delete_stats_summ)) }, + confirmButton = { + TextButton(onClick = { + viewModel.deleteAllTables() + resetGameDataDialog = false + scope.launch { + snackbarHostState.showSnackbar( + context.resources.getString(R.string.action_deleted) + ) + } + }) { + Text( + text = stringResource(R.string.action_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + FilledTonalButton(onClick = { resetGameDataDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + }, + onDismissRequest = { resetGameDataDialog = false } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherViewModel.kt b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherViewModel.kt new file mode 100644 index 00000000..8a378e75 --- /dev/null +++ b/app/src/main/java/com/kaajjo/libresudoku/ui/settings/other/SettingsOtherViewModel.kt @@ -0,0 +1,44 @@ +package com.kaajjo.libresudoku.ui.settings.other + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kaajjo.libresudoku.data.database.AppDatabase +import com.kaajjo.libresudoku.data.datastore.AppSettingsManager +import com.kaajjo.libresudoku.data.datastore.TipCardsDataStore +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsOtherViewModel @Inject constructor( + private val settings: AppSettingsManager, + private val tipCardsDataStore: TipCardsDataStore, + private val appDatabase: AppDatabase +) : ViewModel() { + val saveLastSelectedDifficultyType = settings.saveSelectedGameDifficultyType + fun updateSaveLastSelectedDifficultyType(enabled: Boolean) = + viewModelScope.launch(Dispatchers.IO) { + settings.setSaveSelectedGameDifficultyType(enabled) + } + + val keepScreenOn = settings.keepScreenOn + fun updateKeepScreenOn(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settings.setKeepScreenOn(enabled) + } + } + + fun resetTipCards() { + viewModelScope.launch { + tipCardsDataStore.setStreakCard(true) + tipCardsDataStore.setRecordCard(true) + } + } + + fun deleteAllTables() { + viewModelScope.launch(Dispatchers.IO) { + appDatabase.clearAllTables() + } + } +} diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index f5015b8b..a5b575f8 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -356,4 +356,10 @@ USDT (TRC20) MIR Версия %1$s (%2$s) + Вы можете помочь с переводом на Hosted Webalte + Темная тема, цветовая схема, тема поля, формат даты… + Метод ввода, лимит ошибок, таймер… + Проверка ошибок, подсветка одинаковых чисел, оставшиеся использования… + Запоминать сложность и тип, не выключать экран, сброс статистики… + Применять акцентные цвета, выделение позиции, размер шрифта… \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39b7fa9b..35bedfdc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,12 @@ USDT (TRC20) MIR Version %1$s (%2$s) + You can help with translation on Hosted Weblate + Dark theme, color scheme, board theme, date format… + Input method, mistake limit, timer… + Mistakes checking, highlight identical values, remaining uses… + Remember type and difficulty, keep screen awake, reset stats… + Apply accent colors, position lines, font size… \ No newline at end of file