diff --git a/core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt b/core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt new file mode 100644 index 00000000000..b3d0ec8c580 --- /dev/null +++ b/core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt @@ -0,0 +1,33 @@ +package com.mifos.core.common.enums + +enum class MifosAppLanguage(val code: String, val displayName: String) { + + SYSTEM_LANGUAGE("System_Language", "System Language"), + ENGLISH("en", "English"), + HINDI("hi", "हिंदी"), + ARABIC("ar", "عربى"), + URDU("ur", "اُردُو"), + BENGALI("bn", "বাঙালি"), + SPANISH("es", "Español"), + FRENCH("fr", "français"), + INDONESIAN("in", "bahasa Indonesia"), + KHMER("km", "ភាសាខ្មែរ"), + KANNADA("kn", "ಕನ್ನಡ"), + TELUGU("te", "తెలుగు"), + BURMESE("my", "မြန်မာ"), + POLISH("pl", "Polski"), + PORTUGUESE("pt", "Português"), + RUSSIAN("ru", "русский"), + SWAHILI("sw", "Kiswahili"), + FARSI("fa", "فارسی"); + + companion object { + fun fromCode(code: String): MifosAppLanguage { + return entries.find { it.code.equals(code, ignoreCase = true) } ?: ENGLISH + } + } + + override fun toString(): String { + return displayName + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/utils/Constants.kt b/core/common/src/main/java/com/mifos/core/common/utils/Constants.kt index 66dda6c354b..8a72a668f0b 100644 --- a/core/common/src/main/java/com/mifos/core/common/utils/Constants.kt +++ b/core/common/src/main/java/com/mifos/core/common/utils/Constants.kt @@ -204,4 +204,11 @@ object Constants { const val CURR_PASSWORD = "currentPassword" const val IS_TO_UPDATE_PASS_CODE = "updatePassCode" const val HAS_SETTING_CHANGED = "hasSettingsChanged" + + + const val TENANT = "tenant" + const val BASE_URL = "base_url" + const val PASSCODE = "preferences_mifos_passcode_string" + const val THEME = "theme" + const val LANGUAGE = "language_type" } \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/utils/LanguageHelper.kt b/core/common/src/main/java/com/mifos/core/common/utils/LanguageHelper.kt new file mode 100644 index 00000000000..9c757e5fc3a --- /dev/null +++ b/core/common/src/main/java/com/mifos/core/common/utils/LanguageHelper.kt @@ -0,0 +1,72 @@ +package com.mifos.core.common.utils + +import android.content.Context +import android.os.Build +import android.preference.PreferenceManager +import com.mifos.core.common.R +import java.util.Locale + +object LanguageHelper { + // https://gunhansancar.com/change-language-programmatically-in-android/ + fun onAttach(context: Context): Context? { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + return if (preferences.getBoolean( + context.getString(R.string.core_common_default_system_language), + true + ) + ) { + if (!context.resources.getStringArray(R.array.core_common_languages_value) + .contains(Locale.getDefault().language) + ) { + setLocale(context, "en") + } else { + setLocale(context, Locale.getDefault().language) + } + } else { + val lang = getPersistedData(context, Locale.getDefault().language) + lang?.let { setLocale(context, it) } + } + } + + @JvmStatic + fun onAttach(context: Context, defaultLanguage: String): Context? { + val lang = getPersistedData(context, defaultLanguage) + return lang?.let { setLocale(context, it) } + } + + fun setLocale(context: Context?, language: String): Context? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + updateResources(context!!, language) + } else { + updateResourcesLegacy(context, language) + } + } + + private fun getPersistedData(context: Context, defaultLanguage: String): String? { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + return preferences.getString( + context.getString(R.string.core_common_language_type), + defaultLanguage + ) + } + + private fun updateResources(context: Context, language: String): Context { + val locale = Locale(language) + Locale.setDefault(locale) + val configuration = context.resources.configuration + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + return context.createConfigurationContext(configuration) + } + + private fun updateResourcesLegacy(context: Context?, language: String): Context? { + val locale = Locale(language) + Locale.setDefault(locale) + val resources = context?.resources + val configuration = resources?.configuration + configuration?.locale = locale + configuration?.setLayoutDirection(locale) + resources?.updateConfiguration(configuration, resources.displayMetrics) + return context + } +} \ No newline at end of file diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml new file mode 100644 index 00000000000..b77f01ecbdb --- /dev/null +++ b/core/common/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + + + default_system_language + language_type + + + System_Language + en + hi + ar + ur + bn + es + fr + in + km + kn + te + my + pl + pt + ru + sw + fa + + + \ No newline at end of file diff --git a/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt index 7943da69773..f7db33773b7 100644 --- a/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt +++ b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt @@ -9,6 +9,8 @@ import com.mifos.core.common.utils.Constants import com.mifos.core.common.utils.asServerConfig import com.mifos.core.model.ServerConfig import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.apache.fineract.client.models.PostAuthenticationResponse import org.mifos.core.sharedpreference.Key import org.mifos.core.sharedpreference.UserPreferences @@ -75,4 +77,12 @@ class PrefManager @Inject constructor( fun updateServerConfig(config: ServerConfig?) { this.put(serverConfigKey, config) } + + fun getStringValue(key: String): Flow = flow { + emit(preference.getString(key, "")) + } + + fun setStringValue(key: String,value: String) { + preference.edit().putString(key, value).apply() + } } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt index 76ed83e847e..93cdea9c50b 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt @@ -1,10 +1,33 @@ package com.mifos.core.designsystem.component +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton 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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.mifos.core.designsystem.R @Composable fun MifosDialogBox( @@ -42,3 +65,116 @@ fun MifosDialogBox( ) } } + + +@Composable +fun MifosRadioButtonDialog( + titleResId: Int, + selectedItem: String, + items: Array, + selectItem: (item: String, index: Int) -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = { onDismissRequest.invoke() } + ) { + Card { + Column(modifier = Modifier.padding(20.dp)) { + Text(text = stringResource(id = titleResId)) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 500.dp) + ) { + itemsIndexed(items = items) { index, item -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + onDismissRequest.invoke() + selectItem.invoke(item, index) + } + .fillMaxWidth() + ) { + RadioButton( + selected = (item == selectedItem), + onClick = { + onDismissRequest.invoke() + selectItem.invoke(item, index) + } + ) + Text( + text = item, + modifier = Modifier.padding(start = 4.dp) + ) + } + } + } + } + } + } +} + +@Composable +fun UpdateEndpointDialogScreen( + initialBaseURL: String?, + initialTenant: String?, + onDismissRequest: () -> Unit, + handleEndpointUpdate: (baseURL: String, tenant: String) -> Unit +) { + var baseURL by rememberSaveable { mutableStateOf(initialBaseURL) } + var tenant by rememberSaveable { mutableStateOf(initialTenant) } + + Dialog( + onDismissRequest = { onDismissRequest.invoke() } + ) { + Card { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + ) { + Text(text = stringResource(id = R.string.core_designsystem_pref_base_url_title)) + Spacer(modifier = Modifier.height(8.dp)) + + baseURL?.let { + OutlinedTextField( + value = it, + onValueChange = { baseURL = it }, + label = { Text(text = stringResource(id = R.string.core_designsystem_enter_base_url)) } + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + tenant?.let { + OutlinedTextField( + value = it, + onValueChange = { tenant = it }, + label = { Text(text = stringResource(id = R.string.core_designsystem_enter_tenant)) } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton( + onClick = { onDismissRequest.invoke() }) { + Text(text = stringResource(id = R.string.core_designsystem_cancel)) + } + TextButton( + onClick = { + if (baseURL != null && tenant != null) { + handleEndpointUpdate.invoke(baseURL ?: "", tenant ?: "") + } + } + ) + { + Text(text = stringResource(id = R.string.core_designsystem_dialog_action_ok)) + } + } + } + } + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt index 5e25ba841b8..f8ad407fc9a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt @@ -8,16 +8,19 @@ import androidx.compose.material.icons.outlined.EventRepeat import androidx.compose.material.icons.outlined.Group import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Bedtime import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.FilterList import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Lock import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.PersonOutline import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Sync +import androidx.compose.material.icons.rounded.Translate object MifosIcons { val Add = Icons.Rounded.Add @@ -37,4 +40,7 @@ object MifosIcons { val moreVert = Icons.Rounded.MoreVert val fileTask = Icons.Default.AssignmentTurnedIn val cloudDownload = Icons.Default.CloudDownload + val password = Icons.Rounded.Lock + val theme = Icons.Rounded.Bedtime + val language = Icons.Rounded.Translate } \ No newline at end of file diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 6a600149d1d..8cc4762e160 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -2,4 +2,10 @@ Sorry we weren\'t able to load Try Again + Please enter a base URL + Enter a base URL + Enter a tenant + Cancel + Save + \ No newline at end of file diff --git a/core/ui/src/main/java/com/mifos/core/ui/theme/Theme.kt b/core/ui/src/main/java/com/mifos/core/ui/theme/Theme.kt new file mode 100644 index 00000000000..3f9c7cfd317 --- /dev/null +++ b/core/ui/src/main/java/com/mifos/core/ui/theme/Theme.kt @@ -0,0 +1,52 @@ +package com.mifos.core.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.mifos.core.designsystem.theme.Black +import com.mifos.core.designsystem.theme.BluePrimary +import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.designsystem.theme.BlueSecondary + +private val LightThemeColors = lightColorScheme( + primary = BluePrimary, + onPrimary = Color.White, + error = Color.Red, + background = Color.White, + onSurface = BlueSecondary, + onSecondary = Color.Gray, + outlineVariant = Color.Gray, + surfaceTint = BlueSecondary +) + +private val DarkThemeColors = darkColorScheme( + primary = BluePrimaryDark, + onPrimary = Color.White, + secondary = Black, + error = Color.Red, + background = Color.Black, + surface = BlueSecondary, + onSurface = Color.White, + onSecondary = Color.White, + outlineVariant = Color.White, + surfaceTint = BlueSecondary +) + +@Composable +fun MifosTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colors = when { + useDarkTheme -> DarkThemeColors + else -> LightThemeColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 7a0a9f38c1b..98c64985680 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -12,6 +12,8 @@ dependencies { implementation(projects.core.datastore) implementation(projects.core.domain) + implementation(projects.core.ui) + implementation(libs.appcompat) androidTestImplementation(libs.androidx.compose.ui.test) debugApi(libs.androidx.compose.ui.testManifest) diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt new file mode 100644 index 00000000000..33dbf8e8e28 --- /dev/null +++ b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt @@ -0,0 +1,265 @@ +package com.mifos.feature.settings.settings + +import android.content.Context +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.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.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.common.enums.MifosAppLanguage +import com.mifos.core.common.utils.LanguageHelper +import com.mifos.core.designsystem.component.MifosRadioButtonDialog +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.UpdateEndpointDialogScreen +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.feature.settings.R +import java.util.Locale + +@Composable +fun SettingsScreen( + onBackPressed: () -> Unit, + navigateToLoginScreen: () -> Unit, + changePasscode: (String) -> Unit, + languageChanged: () -> Unit, + serverConfig: () -> Unit +) { + val viewModel: SettingsViewModel = hiltViewModel() + val baseURL by viewModel.baseUrl.collectAsStateWithLifecycle() + val tenant by viewModel.tenant.collectAsStateWithLifecycle() + val passcode by viewModel.passcode.collectAsStateWithLifecycle() + val theme by viewModel.theme.collectAsStateWithLifecycle() + val language by viewModel.language.collectAsStateWithLifecycle() + val context = LocalContext.current + + SettingsScreen( + onBackPressed = onBackPressed, + selectedLanguage = language ?: "System Language", + selectedTheme = theme ?: "System Theme", + baseURL = baseURL ?: "", + tenant = tenant ?: "", + changePasscode = { changePasscode(passcode ?: "") }, + handleEndpointUpdate = { baseURL, tenant -> + if (viewModel.tryUpdatingEndpoint(selectedBaseUrl = baseURL, selectedTenant = tenant)) { + navigateToLoginScreen() + } + }, + updateTheme = { + viewModel.updateTheme(it) + }, + updateLanguage = { + val isSystemLanguage = viewModel.updateLanguage(it.code) + updateLanguageLocale( + context = context, + language = it.code, + isSystemLanguage = isSystemLanguage + ) + languageChanged() + }, + serverConfig = serverConfig + ) +} + + +@Composable +fun SettingsScreen( + onBackPressed: () -> Unit, + selectedLanguage: String, + selectedTheme: String, + baseURL: String, + tenant: String, + changePasscode: () -> Unit, + handleEndpointUpdate: (baseURL: String, tenant: String) -> Unit, + updateTheme: (theme: AppTheme) -> Unit, + updateLanguage: (language: MifosAppLanguage) -> Unit, + serverConfig: () -> Unit +) { + + val snackbarHostState = remember { SnackbarHostState() } + var showLanguageUpdateDialog by rememberSaveable { mutableStateOf(false) } + var showEndpointUpdateDialog by rememberSaveable { mutableStateOf(false) } + var showThemeUpdateDialog by rememberSaveable { mutableStateOf(false) } + + MifosScaffold( + icon = MifosIcons.arrowBack, + title = stringResource(id = R.string.feature_settings), + onBackPressed = onBackPressed, + snackbarHostState = snackbarHostState, + ) { paddingValues -> + Column( + Modifier.padding(paddingValues) + ) { + SettingsCards( + settingsCardClicked = { item -> + when (item) { + SettingsCardItem.SYNC_SURVEY -> { + // TODO Implement Survey dialog + } + + SettingsCardItem.LANGUAGE -> showLanguageUpdateDialog = true + + SettingsCardItem.THEME -> showThemeUpdateDialog = true + + SettingsCardItem.PASSCODE -> changePasscode() + + SettingsCardItem.ENDPOINT -> showEndpointUpdateDialog = true + + SettingsCardItem.SERVER_CONFIG -> { + serverConfig() + + } + } + } + ) + } + } + + if (showLanguageUpdateDialog) { + MifosRadioButtonDialog( + titleResId = R.string.feature_settings_choose_language, + items = stringArrayResource(R.array.feature_settings_languages), + selectItem = { _, index -> updateLanguage(MifosAppLanguage.entries[index]) }, + onDismissRequest = { showLanguageUpdateDialog = false }, + selectedItem = MifosAppLanguage.fromCode(selectedLanguage).displayName + ) + } + + if (showThemeUpdateDialog) { + MifosRadioButtonDialog( + titleResId = R.string.feature_settings_change_app_theme, + items = AppTheme.entries.map { it.themeName }.toTypedArray(), + selectItem = { _, index -> updateTheme(AppTheme.entries[index]) }, + onDismissRequest = { showThemeUpdateDialog = false }, + selectedItem = selectedTheme + ) + } + + if (showEndpointUpdateDialog) { + UpdateEndpointDialogScreen( + initialBaseURL = baseURL, + initialTenant = tenant, + onDismissRequest = { showEndpointUpdateDialog = false }, + handleEndpointUpdate = handleEndpointUpdate + ) + } +} + +@Composable +fun SettingsCards( + settingsCardClicked: (SettingsCardItem) -> Unit, +) { + LazyColumn { + items(SettingsCardItem.entries) { card -> + SettingsCardItem( + title = card.title, + details = card.details, + icon = card.icon, + onclick = { + settingsCardClicked(card) + } + ) + } + } +} + +@Composable +fun SettingsCardItem( + title: Int, + details: Int, + icon: ImageVector?, + onclick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + shape = RoundedCornerShape(0.dp), + onClick = { onclick.invoke() } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 16.dp), + ) { + icon?.let { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier.weight(0.2f) + ) + } + if (icon == null) { + Spacer(modifier = Modifier.weight(0.2f)) + } + Column( + modifier = Modifier.weight(0.8f) + ) { + Text( + text = stringResource(id = title), + style = MaterialTheme.typography.bodyMedium + ) + Text( + modifier = Modifier.padding(end = 16.dp), + text = stringResource(id = details), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } +} + + +fun updateLanguageLocale(context: Context, language: String, isSystemLanguage: Boolean) { + if (isSystemLanguage) { + LanguageHelper.setLocale(context, language) + } else { + val systemLanguageCode = Locale.getDefault().language + if (MifosAppLanguage.entries.find { it.code == systemLanguageCode } == null) { + LanguageHelper.setLocale(context, MifosAppLanguage.ENGLISH.code) + } else { + LanguageHelper.setLocale(context, language) + } + } +} + +@Composable +@Preview(showSystemUi = true, showBackground = true) +fun PreviewSettingsScreen() { + SettingsScreen( + onBackPressed = {}, + selectedLanguage = "", + selectedTheme = "", + baseURL = "", + tenant = "", + handleEndpointUpdate = { _, _ -> }, + updateLanguage = {}, + updateTheme = {}, + changePasscode = {}, + serverConfig = {} + ) + +} \ No newline at end of file diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt new file mode 100644 index 00000000000..b8f74247cae --- /dev/null +++ b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt @@ -0,0 +1,107 @@ +package com.mifos.feature.settings.settings + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.enums.MifosAppLanguage +import com.mifos.core.common.utils.Constants +import com.mifos.core.datastore.PrefManager +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.feature.settings.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val prefManager: PrefManager +) : ViewModel() { + + val tenant: StateFlow = prefManager.getStringValue(Constants.TENANT) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + val baseUrl: StateFlow = prefManager.getStringValue(Constants.BASE_URL) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + val passcode: StateFlow = prefManager.getStringValue(Constants.PASSCODE) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + val theme: StateFlow = prefManager.getStringValue(Constants.THEME) + .stateIn(viewModelScope, SharingStarted.Eagerly, "System Theme") + + val language: StateFlow = prefManager.getStringValue(Constants.LANGUAGE) + .stateIn(viewModelScope, SharingStarted.Eagerly, "System Language") + + + fun updateTheme(theme: AppTheme) { + prefManager.setStringValue(Constants.THEME, theme.themeName) + AppCompatDelegate.setDefaultNightMode( + when (theme) { + AppTheme.DARK -> AppCompatDelegate.MODE_NIGHT_YES + AppTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + ) + } + + fun updateLanguage(language: String): Boolean { + prefManager.setStringValue(Constants.LANGUAGE, language) + val isSystemLanguage = (language == MifosAppLanguage.SYSTEM_LANGUAGE.code) + return isSystemLanguage + } + + fun tryUpdatingEndpoint(selectedBaseUrl: String, selectedTenant: String): Boolean { + // TODO Implement endpoint update + return !(baseUrl.equals(selectedBaseUrl) && tenant.equals(selectedTenant)) + } + +} + + +enum class SettingsCardItem( + val title: Int, + val details: Int, + val icon: ImageVector? +) { + SYNC_SURVEY( + title = R.string.feature_settings_sync_survey, + details = R.string.feature_settings_sync_survey_desc, + icon = null + ), + LANGUAGE( + title = R.string.feature_settings_language, + details = R.string.feature_settings_language_desc, + icon = MifosIcons.language + ), + THEME( + title = R.string.feature_settings_theme, + details = R.string.feature_settings_theme_desc, + icon = MifosIcons.theme + ), + PASSCODE( + title = R.string.feature_settings_change_passcode, + details = R.string.feature_settings_change_passcode_desc, + icon = MifosIcons.password + ), + ENDPOINT( + title = R.string.feature_settings_instance_url, + details = R.string.feature_settings_instance_url_desc, + icon = null + ), + SERVER_CONFIG( + title = R.string.feature_settings_server_config, + details = R.string.feature_settings_server_config_desc, + icon = null + ) +} + +enum class AppTheme( + val themeName: String +) { + SYSTEM(themeName = "System Theme"), + LIGHT(themeName = "Light Theme"), + DARK(themeName = "Dark Theme") +} \ No newline at end of file diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 98ca04e7e6c..d7f6ed09777 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -3,4 +3,43 @@ CloseBottomSheetIcon Update Config The application will restart after a successful update. + + Settings + Sync Survey + Click to Sync Survey + Language + Select your preferred language + Theme + Select theme + Change Passcode + Change App Passcode + Instance URL + Update Instance URL + Server Config + Update Server Config + Choose Language + Change App Theme + + + + System Language + English + हिंदी + عربى + اُردُو + বাঙালি + Español + français + bahasa Indonesia + ភាសាខ្មែរ + ಕನ್ನಡ + తెలుగు + မြန်မာ + Polski + Português + русский + Kiswahili + فارسی + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c5e69578a0..3979d6e44c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -119,6 +119,7 @@ turbine = "1.0.0" timberVersion = '5.0.1' truthVersion = '1.1.5' runtimeLivedata = "1.6.8" +appcompat = "1.7.0" [libraries] # AndroidX Libraries @@ -371,6 +372,7 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } [plugins] diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/setting/SettingsFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/setting/SettingsFragment.kt index c2c502a3f89..de544c9c485 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/setting/SettingsFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/setting/SettingsFragment.kt @@ -1,160 +1,75 @@ -@file:Suppress("DEPRECATION") - package com.mifos.mifosxdroid.activity.setting import android.content.Intent -import android.content.SharedPreferences import android.os.Bundle -import android.widget.Toast +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.Preference.OnPreferenceChangeListener -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreference +import com.mifos.core.common.utils.Constants +import com.mifos.feature.settings.settings.SettingsScreen import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.dialogfragments.syncsurveysdialog.SyncSurveysDialogFragment -import com.mifos.mifosxdroid.online.DashboardActivity +import com.mifos.mifosxdroid.activity.login.LoginActivity import com.mifos.mifosxdroid.passcode.PassCodeActivity import com.mifos.mobile.passcode.utils.PasscodePreferencesHelper -import com.mifos.utils.Constants -import com.mifos.utils.FragmentConstants -import com.mifos.utils.LanguageHelper -import com.mifos.utils.PrefManager -import com.mifos.utils.ThemeHelper /** * Created by mayankjindal on 22/07/17. */ -class SettingsFragment : PreferenceFragmentCompat(), - SharedPreferences.OnSharedPreferenceChangeListener { - private lateinit var mEnableSyncSurvey: SwitchPreference - private lateinit var mInstanceUrlPref: EditTextPreference - private lateinit var languages: Array - private var languageCallback: LanguageCallback? = null - private var mRootKey: String? = null - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.preferences, rootKey) - mRootKey = rootKey - languages = requireActivity().resources.getStringArray(R.array.language_option) - - initSurveyPreferences() - initInstanceUrlPreferences() - initLanguagePreferences() - initThemePreferences() - } - - private fun initSurveyPreferences() { - mEnableSyncSurvey = - (findPreference(requireContext().getString(R.string.sync_survey)) as SwitchPreference?)!! - mEnableSyncSurvey.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> - if (newValue as Boolean) { - val syncSurveysDialogFragment = SyncSurveysDialogFragment.newInstance() - val fragmentTransaction = parentFragmentManager.beginTransaction() - fragmentTransaction.addToBackStack(FragmentConstants.FRAG_SURVEYS_SYNC) - syncSurveysDialogFragment.isCancelable = false - fragmentTransaction.let { - syncSurveysDialogFragment.show( - it, - requireContext().getString(R.string.sync_clients) - ) - } - } - true - } - } - - private fun initInstanceUrlPreferences() { - mInstanceUrlPref = (findPreference( - requireContext().getString(R.string.hint_instance_url) - ) as EditTextPreference?)!! - val instanceUrl = PrefManager.getInstanceUrl() - mInstanceUrlPref.text = instanceUrl - mInstanceUrlPref.isSelectable = true - mInstanceUrlPref.dialogTitle = "Edit Instance Url" - mInstanceUrlPref.setDialogIcon(R.drawable.ic_baseline_edit_24) - mInstanceUrlPref.onPreferenceChangeListener = - OnPreferenceChangeListener { _, o -> - val newUrl = o.toString() - if (newUrl != instanceUrl) { - PrefManager.setInstanceUrl(newUrl) - Toast.makeText(activity, newUrl, Toast.LENGTH_SHORT).show() - startActivity(Intent(activity, DashboardActivity::class.java)) - activity?.finishAffinity() - } - true +class SettingsFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + SettingsScreen( + onBackPressed = { + findNavController().popBackStack() + }, + navigateToLoginScreen = { + Intent(requireContext(), LoginActivity::class.java).also { + startActivity(it) + } + }, + changePasscode = { + val passCodePreferencesHelper = PasscodePreferencesHelper(activity) + val currPassCode = passCodePreferencesHelper.passCode + passCodePreferencesHelper.savePassCode("") + val intent = Intent(requireContext(), PassCodeActivity::class.java).apply { + putExtra(Constants.CURR_PASSWORD, currPassCode) + putExtra(Constants.IS_TO_UPDATE_PASS_CODE, true) + } + startActivity(intent) + }, + languageChanged = { + val intent = Intent(activity, activity?.javaClass) + intent.putExtra(Constants.HAS_SETTING_CHANGED, true) + startActivity(intent) + activity?.finish() + }, + serverConfig = { + findNavController().navigate(R.id.updateServerConfigFragment) + } + ) } - } - - private fun initLanguagePreferences() { - val langPref = findPreference("language_type") as ListPreference? - langPref?.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> - LanguageHelper.setLocale(requireActivity(), newValue.toString()) - val intent = Intent(requireActivity(), requireActivity().javaClass) - intent.putExtra(Constants.HAS_SETTING_CHANGED, true) - startActivity(intent) - requireActivity().finish() - preferenceScreen = null - setPreferencesFromResource(R.xml.preferences, mRootKey) - preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) - true } } - private fun initThemePreferences() { - val themePreference = - findPreference(requireContext().getString(R.string.mode_key)) as ListPreference? - themePreference?.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> - val themeOption = newValue as String - ThemeHelper.applyTheme(themeOption) - Toast.makeText(activity, "Switched to $themeOption Mode", Toast.LENGTH_SHORT).show() - true - } + override fun onResume() { + super.onResume() + (requireActivity() as AppCompatActivity).supportActionBar?.hide() } - override fun onPause() { - super.onPause() - preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) - } - - fun setLanguageCallback(languageCallback: LanguageCallback?) { - this.languageCallback = languageCallback - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, s: String) { - val preference = findPreference(s) as ListPreference? - LanguageHelper.setLocale(requireActivity(), preference?.value) - } - - interface LanguageCallback - - override fun onPreferenceTreeClick( - preference: Preference - ): Boolean { - when (preference.key) { - getString(R.string.password) -> { - // TODO: create changePasswordActivity and implement the logic for password change - } - - getString(R.string.passcode) -> { - activity?.let { - val passCodePreferencesHelper = PasscodePreferencesHelper(activity) - val currPassCode = passCodePreferencesHelper.passCode - passCodePreferencesHelper.savePassCode("") - val intent = Intent(it, PassCodeActivity::class.java).apply { - putExtra(Constants.CURR_PASSWORD, currPassCode) - putExtra(Constants.IS_TO_UPDATE_PASS_CODE, true) - } - startActivity(intent) - } - } - - getString(R.string.updateServerConfig) -> { - findNavController().navigate(R.id.updateServerConfigFragment) - } - } - return super.onPreferenceTreeClick(preference) + override fun onStop() { + super.onStop() + (requireActivity() as AppCompatActivity).supportActionBar?.show() } }