diff --git a/app/src/main/java/com/washingtondcsquad/tudee/MainActivity.kt b/app/src/main/java/com/washingtondcsquad/tudee/MainActivity.kt index e37b598..76e4ad1 100644 --- a/app/src/main/java/com/washingtondcsquad/tudee/MainActivity.kt +++ b/app/src/main/java/com/washingtondcsquad/tudee/MainActivity.kt @@ -8,7 +8,6 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -19,12 +18,10 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -36,6 +33,7 @@ import androidx.navigation.compose.rememberNavController import com.washingtondcsquad.tudee.data.localSource.TudeeDataBase import com.washingtondcsquad.tudee.domain.entity.Category import com.washingtondcsquad.tudee.domain.services.AppPreferencesService +import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesScreen import com.washingtondcsquad.tudee.presentation.components.SnackBarCard import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.TudeeNavigationBar import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.bottomNavBarRoutes @@ -43,7 +41,6 @@ import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.navBar import com.washingtondcsquad.tudee.presentation.components.snack_bar.ObserveAsEvent import com.washingtondcsquad.tudee.presentation.components.snack_bar.SnackbarController import com.washingtondcsquad.tudee.presentation.design.AppTheme -import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesScreen import com.washingtondcsquad.tudee.presentation.features.home.HomeScreen import com.washingtondcsquad.tudee.presentation.features.onBoarding.OnBoardingScreen import com.washingtondcsquad.tudee.presentation.features.tasks_screen.TasksScreen @@ -57,42 +54,29 @@ val LocalInnerPaddingProvider = @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") class MainActivity : ComponentActivity() { - private val appPreferencesService: AppPreferencesService by inject() - + private val mainViewModel: MainViewModel by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - val isDarkMode by appPreferencesService.isDarkModeEnabled() - .collectAsState(initial = false) - val isOnboardingShownFlow = appPreferencesService.hasOnboardingBeenShown() - val isOnboardingShownState by isOnboardingShownFlow.collectAsState(initial = null) + val mainState by mainViewModel.state.collectAsState() + updateAppLocale(mainState) AppTheme( - useDarkTheme = isDarkMode + useDarkTheme = mainState.isDarkTheme ) { - when (val isOnboardingShown = isOnboardingShownState) { - null -> { - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - - else -> { - val startDestination = if (isOnboardingShown) { - "home" - } else { - Log.d("sdasdsad", "onCreate: ") - /// createPreDefineCategories() - "onboarding" - } - val navController = rememberNavController() - val navBackStackEntry = navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry.value?.destination?.route + val isOnboardingShown = mainState.hasOnBoardingShown + val startDestination = if (isOnboardingShown) { + "home" + } else { + Log.d("sdasdsad", "onCreate: ") + "onboarding" + } + val navController = rememberNavController() + val navBackStackEntry = navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry.value?.destination?.route Scaffold( bottomBar = { @@ -137,7 +121,7 @@ class MainActivity : ComponentActivity() { } } } - SnackbarHandler() + SnackBarHandler() } } // @@ -170,49 +154,13 @@ class MainActivity : ComponentActivity() { // } // } // } -} - -@Composable -fun SnackbarHandler() { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - ObserveAsEvent( - flow = SnackbarController.event, - onEvent = { event -> - coroutineScope.launch { - snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar( - message = event.message, - duration = SnackbarDuration.Short - ) - } - } - ) - - SnackbarHost( - hostState = snackbarHostState, - snackbar = { snackbarData -> - val (iconRes, iconTint) = when { - snackbarData.visuals.message.contains("success", ignoreCase = true) -> - Pair(R.drawable.checkmark, AppTheme.colors.greenAccent) - - snackbarData.visuals.message.contains("error", ignoreCase = true) -> - Pair(R.drawable.checkmark, AppTheme.colors.error) - - else -> - Pair(R.drawable.information_diamond, AppTheme.colors.error) - } + private fun updateAppLocale(state: MainActivityUiState) { + AppCompatDelegate.setApplicationLocales( + LocaleListCompat.forLanguageTags(state.currentAppLocale.toLanguageTag()) + ) + } +} - SnackBarCard( - message = snackbarData.visuals.message, - icon = painterResource(id = iconRes), - iconTint = iconTint, - iconBackgroundColor = AppTheme.colors.surface, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 48.dp) - ) - } - ) -} diff --git a/app/src/main/java/com/washingtondcsquad/tudee/MainActivityUiState.kt b/app/src/main/java/com/washingtondcsquad/tudee/MainActivityUiState.kt new file mode 100644 index 0000000..2ef0469 --- /dev/null +++ b/app/src/main/java/com/washingtondcsquad/tudee/MainActivityUiState.kt @@ -0,0 +1,9 @@ +package com.washingtondcsquad.tudee + +import java.util.Locale + +data class MainActivityUiState( + val isDarkTheme: Boolean = false, + val hasOnBoardingShown: Boolean = false, + val currentAppLocale: Locale = Locale.ENGLISH +) diff --git a/app/src/main/java/com/washingtondcsquad/tudee/MainViewModel.kt b/app/src/main/java/com/washingtondcsquad/tudee/MainViewModel.kt new file mode 100644 index 0000000..9d83cd0 --- /dev/null +++ b/app/src/main/java/com/washingtondcsquad/tudee/MainViewModel.kt @@ -0,0 +1,72 @@ +package com.washingtondcsquad.tudee + +import androidx.lifecycle.viewModelScope +import com.washingtondcsquad.tudee.domain.services.AppPreferencesService +import com.washingtondcsquad.tudee.presentation.base.BaseViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.Locale + +class MainViewModel( + private val appPreferencesService: AppPreferencesService +) : BaseViewModel(MainActivityUiState()) { + + init { + getIsDarkTheme() + getOnBoardingState() + getAppLocale() + } + + private fun getIsDarkTheme() { + tryToExecute( + request = { + appPreferencesService.isDarkModeEnabled() + }, + onSuccess = ::onDarkThemeEnabled, + onError = {} + ) + } + + private fun onDarkThemeEnabled(isDarkThemeEnabled: Flow) = viewModelScope.launch { + isDarkThemeEnabled.collectLatest { + updateState { + copy(isDarkTheme = it) + } + } + } + + private fun getOnBoardingState() { + tryToExecute( + request = { + appPreferencesService.hasOnboardingBeenShown() + }, + onSuccess = ::onOnBoardingState, + onError = {} + ) + } + + private fun onOnBoardingState(hasOnboardingBeenShown: Flow) = viewModelScope.launch { + hasOnboardingBeenShown.collectLatest { + updateState { + copy(hasOnBoardingShown = it) + } + } + } + + private fun getAppLocale() { + tryToCollect( + request = { + appPreferencesService.getCurrentLocale() + }, + onChange = ::onAppLanguageChanged, + onError = {}, + ) + } + + private fun onAppLanguageChanged(newLocale: Locale) { + updateState { + copy(currentAppLocale = newLocale) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/washingtondcsquad/tudee/data/services/AppPreferencesServiceImpl.kt b/app/src/main/java/com/washingtondcsquad/tudee/data/services/AppPreferencesServiceImpl.kt index f73caf7..d7bf90b 100644 --- a/app/src/main/java/com/washingtondcsquad/tudee/data/services/AppPreferencesServiceImpl.kt +++ b/app/src/main/java/com/washingtondcsquad/tudee/data/services/AppPreferencesServiceImpl.kt @@ -1,17 +1,37 @@ package com.washingtondcsquad.tudee.data.services +import androidx.appcompat.app.AppCompatDelegate import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import com.washingtondcsquad.tudee.domain.services.AppPreferencesService import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import java.util.Locale class AppPreferencesServiceImpl( private val dataStore: DataStore ) : AppPreferencesService { + private val _currentLocale = MutableStateFlow(getCurrentAppLocale()) + val currentLocale: Flow = _currentLocale.asStateFlow() + + override suspend fun getCurrentLocale(): Flow = currentLocale.distinctUntilChanged() + + + private fun getCurrentAppLocale(): Locale { + val androidLocale = AppCompatDelegate.getApplicationLocales()[0] + ?: Locale.getDefault() + return Locale.Builder() + .setLocale(androidLocale) + .setLanguage(androidLocale.language) + .build() + } + companion object PreferencesKeys { val HAS_ONBOARDING_BEEN_SHOWN = booleanPreferencesKey("has_onboarding_been_shown") val DARK_MODE_ENABLED = booleanPreferencesKey("dark_mode_enabled") diff --git a/app/src/main/java/com/washingtondcsquad/tudee/di/viewModelModule.kt b/app/src/main/java/com/washingtondcsquad/tudee/di/viewModelModule.kt index 8ee5485..5c8ebef 100644 --- a/app/src/main/java/com/washingtondcsquad/tudee/di/viewModelModule.kt +++ b/app/src/main/java/com/washingtondcsquad/tudee/di/viewModelModule.kt @@ -1,25 +1,29 @@ package com.washingtondcsquad.tudee.di +import com.washingtondcsquad.tudee.MainViewModel import com.washingtondcsquad.tudee.presentation.features.add_task.AddTaskViewModel +import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesViewModel +import com.washingtondcsquad.tudee.presentation.features.edit_task.EditTaskViewModel import com.washingtondcsquad.tudee.presentation.features.home.HomeViewModel -import com.washingtondcsquad.tudee.presentation.features.task_details.BottomSheetTaskViewModel import com.washingtondcsquad.tudee.presentation.features.onBoarding.OnboardingViewModel -import com.washingtondcsquad.tudee.presentation.features.edit_task.EditTaskViewModel +import com.washingtondcsquad.tudee.presentation.features.task_details.BottomSheetTaskViewModel import com.washingtondcsquad.tudee.presentation.features.tasks_screen.TasksViewModel import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module -import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesViewModel val viewModelModule = module { viewModelOf(::CategoriesViewModel) viewModelOf(::OnboardingViewModel) viewModelOf(::HomeViewModel) - viewModel{ TasksViewModel( - get(), - get() - ) } + viewModel { + TasksViewModel( + get(), + get() + ) + } + viewModelOf(::MainViewModel) viewModel { BottomSheetTaskViewModel(get()) } viewModelOf(::AddTaskViewModel) diff --git a/app/src/main/java/com/washingtondcsquad/tudee/domain/services/AppPreferencesService.kt b/app/src/main/java/com/washingtondcsquad/tudee/domain/services/AppPreferencesService.kt index 41f7468..8fece7c 100644 --- a/app/src/main/java/com/washingtondcsquad/tudee/domain/services/AppPreferencesService.kt +++ b/app/src/main/java/com/washingtondcsquad/tudee/domain/services/AppPreferencesService.kt @@ -1,8 +1,10 @@ package com.washingtondcsquad.tudee.domain.services import kotlinx.coroutines.flow.Flow +import java.util.Locale interface AppPreferencesService { + suspend fun getCurrentLocale(): Flow fun hasOnboardingBeenShown(): Flow suspend fun setOnboardingShown() fun isDarkModeEnabled(): Flow diff --git a/app/src/main/java/com/washingtondcsquad/tudee/presentation/utils/SnackBar.kt b/app/src/main/java/com/washingtondcsquad/tudee/presentation/utils/SnackBar.kt new file mode 100644 index 0000000..463817d --- /dev/null +++ b/app/src/main/java/com/washingtondcsquad/tudee/presentation/utils/SnackBar.kt @@ -0,0 +1,61 @@ +package com.washingtondcsquad.tudee.presentation.utils + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.washingtondcsquad.tudee.R +import com.washingtondcsquad.tudee.presentation.components.SnackBarCard +import com.washingtondcsquad.tudee.presentation.components.snack_bar.ObserveAsEvent +import com.washingtondcsquad.tudee.presentation.components.snack_bar.SnackbarController +import com.washingtondcsquad.tudee.presentation.design.AppTheme +import kotlinx.coroutines.launch + +@Composable +fun SnackBarHandler() { + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + ObserveAsEvent( + flow = SnackbarController.event, + onEvent = { event -> + coroutineScope.launch { + snackBarHostState.currentSnackbarData?.dismiss() + snackBarHostState.showSnackbar( + message = event.message, + duration = SnackbarDuration.Short + ) + } + } + ) + + SnackbarHost( + hostState = snackBarHostState, + snackbar = { snackbarData -> + val (iconRes, iconTint) = when { + snackbarData.visuals.message.contains("success", ignoreCase = true) -> + Pair(R.drawable.checkmark, AppTheme.colors.greenAccent) + + snackbarData.visuals.message.contains("error", ignoreCase = true) -> + Pair(R.drawable.checkmark, AppTheme.colors.error) + + else -> + Pair(R.drawable.information_diamond, AppTheme.colors.error) + } + + SnackBarCard( + message = snackbarData.visuals.message, + icon = painterResource(id = iconRes), + iconTint = iconTint, + iconBackgroundColor = AppTheme.colors.surface, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 48.dp) + ) + + } + ) +}