diff --git a/app/build.gradle b/app/build.gradle index 696cc08..cac882c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -127,6 +127,9 @@ dependencies { //Permissions implementation "com.google.accompanist:accompanist-permissions:0.23.1" + // DataStore + implementation "androidx.datastore:datastore-preferences:1.0.0" + // Room def roomVersion = "2.6.1" implementation "androidx.room:room-runtime:$roomVersion" diff --git a/app/src/main/java/com/bera/josaahelpertool/MainActivity.kt b/app/src/main/java/com/bera/josaahelpertool/MainActivity.kt index 3d332c7..9ab3446 100644 --- a/app/src/main/java/com/bera/josaahelpertool/MainActivity.kt +++ b/app/src/main/java/com/bera/josaahelpertool/MainActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.collectAsState @@ -17,6 +18,7 @@ import com.bera.josaahelpertool.network.connectivity.ConnectivityObserver import com.bera.josaahelpertool.network.connectivity.ConnectivityStatus import com.bera.josaahelpertool.screens.home.NetworkErrorScreen import com.bera.josaahelpertool.ui.theme.CollegeSearchTheme +import com.bera.josaahelpertool.ui.theme.ThemeViewModel import dagger.hilt.android.AndroidEntryPoint import java.io.File import javax.inject.Inject @@ -25,13 +27,19 @@ import javax.inject.Inject class MainActivity : ComponentActivity() { @Inject lateinit var connectivityObserver: ConnectivityObserver + private val themeViewModel: ThemeViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val darkTheme = themeViewModel.shouldUseDarkTheme() + val uiState by themeViewModel.uiState.collectAsState() - CollegeSearchTheme { + CollegeSearchTheme( + darkTheme = darkTheme, + dynamicColor = uiState.dynamicColor + ) { val status by connectivityObserver.observe() .collectAsState(initial = ConnectivityStatus.Unavailable) diff --git a/app/src/main/java/com/bera/josaahelpertool/di/AppModule.kt b/app/src/main/java/com/bera/josaahelpertool/di/AppModule.kt index 1531e9b..c89621d 100644 --- a/app/src/main/java/com/bera/josaahelpertool/di/AppModule.kt +++ b/app/src/main/java/com/bera/josaahelpertool/di/AppModule.kt @@ -9,6 +9,7 @@ import com.bera.josaahelpertool.network.okhttp.CacheInterceptor import com.bera.josaahelpertool.network.okhttp.ForceCacheInterceptor import com.bera.josaahelpertool.repository.CutoffRepository import com.bera.josaahelpertool.repository.UniversityImageRepository +import com.bera.josaahelpertool.ui.theme.ThemeDataStore import com.bera.josaahelpertool.utils.Constants import dagger.Module import dagger.Provides @@ -80,4 +81,9 @@ object AppModule { @Singleton @Provides fun provideContext(@ApplicationContext appContext: Context): Context = appContext + + @Singleton + @Provides + fun provideThemeDataStore(@ApplicationContext appContext: Context): ThemeDataStore = + ThemeDataStore(appContext) } \ No newline at end of file diff --git a/app/src/main/java/com/bera/josaahelpertool/navigation/Navigation.kt b/app/src/main/java/com/bera/josaahelpertool/navigation/Navigation.kt index c663f22..957bc0f 100644 --- a/app/src/main/java/com/bera/josaahelpertool/navigation/Navigation.kt +++ b/app/src/main/java/com/bera/josaahelpertool/navigation/Navigation.kt @@ -19,6 +19,7 @@ import com.bera.josaahelpertool.screens.home.HomeScreen import com.bera.josaahelpertool.screens.home.HomeViewModel import com.bera.josaahelpertool.screens.search.SearchScreen import com.bera.josaahelpertool.screens.search.SearchViewModel +import com.bera.josaahelpertool.ui.theme.ThemeViewModel @Composable fun Navigation() { @@ -28,7 +29,8 @@ fun Navigation() { ) { composable(Routes.HomeScreen.route) { val homeViewModel = hiltViewModel() - HomeScreen(navController = navController, viewModel = homeViewModel) + val themeViewModel = hiltViewModel() + HomeScreen(navController = navController, viewModel = homeViewModel, themeViewModel = themeViewModel) } composable(Routes.CollegeScreen.route + "/{category}", listOf(navArgument("category") { type = NavType.StringType diff --git a/app/src/main/java/com/bera/josaahelpertool/screens/home/HomeScreen.kt b/app/src/main/java/com/bera/josaahelpertool/screens/home/HomeScreen.kt index 8f17aab..de0de69 100644 --- a/app/src/main/java/com/bera/josaahelpertool/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/bera/josaahelpertool/screens/home/HomeScreen.kt @@ -65,8 +65,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage +import com.bera.josaahelpertool.components.ThemeSwitcher import com.bera.josaahelpertool.models.ui.TopHalfItem import com.bera.josaahelpertool.navigation.Routes +import com.bera.josaahelpertool.ui.theme.ThemeViewModel import com.bera.josaahelpertool.ui.theme.rubikFamily import com.bera.josaahelpertool.utils.CustomDivider import com.bera.josaahelpertool.utils.ShimmerListItem @@ -79,7 +81,8 @@ import kotlin.reflect.KSuspendFunction2 @Composable fun HomeScreen( navController: NavController, - viewModel: HomeViewModel + viewModel: HomeViewModel, + themeViewModel: ThemeViewModel ) { @@ -110,6 +113,26 @@ fun HomeScreen( item { Spacer(modifier = Modifier.height(12.dp)) } + item { + // Theme Switcher + val isDarkTheme = themeViewModel.shouldUseDarkTheme() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.End + ) { + ThemeSwitcher( + darkTheme = isDarkTheme, + size = 30.dp, + onClick = { themeViewModel.toggleTheme() } + ) + } + } + item { + Spacer(modifier = Modifier.height(12.dp)) + } item { // Create a search bar to type and search for colleges Row( diff --git a/app/src/main/java/com/bera/josaahelpertool/ui/theme/Color.kt b/app/src/main/java/com/bera/josaahelpertool/ui/theme/Color.kt index 4b54ec3..e2819e1 100644 --- a/app/src/main/java/com/bera/josaahelpertool/ui/theme/Color.kt +++ b/app/src/main/java/com/bera/josaahelpertool/ui/theme/Color.kt @@ -33,36 +33,35 @@ val md_theme_light_surfaceTint = Color(0xFF0062A1) val md_theme_light_outlineVariant = Color(0xFFC2C7CF) val md_theme_light_scrim = Color(0xFF000000) -//val md_theme_dark_primary = Color(0xFF9DCAFF) -//val md_theme_dark_onPrimary = Color(0xFF003257) -//val md_theme_dark_primaryContainer = Color(0xFF00497B) -//val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF) -//val md_theme_dark_secondary = Color(0xFFBAC8DB) -//val md_theme_dark_onSecondary = Color(0xFF253140) -//val md_theme_dark_secondaryContainer = Color(0xFF3B4858) -//val md_theme_dark_onSecondaryContainer = Color(0xFFD6E4F7) -//val md_theme_dark_tertiary = Color(0xFFB2C5FF) -//val md_theme_dark_onTertiary = Color(0xFF002B73) -//val md_theme_dark_tertiaryContainer = Color(0xFF1E438F) -//val md_theme_dark_onTertiaryContainer = Color(0xFFDAE2FF) -//val md_theme_dark_error = Color(0xFFFFB4AB) -//val md_theme_dark_errorContainer = Color(0xFF93000A) -//val md_theme_dark_onError = Color(0xFF690005) -//val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -//val md_theme_dark_background = Color(0xFF1A1C1E) -//val md_theme_dark_onBackground = Color(0xFFE2E2E6) -//val md_theme_dark_surface = Color(0xFF1A1C1E) -//val md_theme_dark_onSurface = Color(0xFFE2E2E6) -//val md_theme_dark_surfaceVariant = Color(0xFF42474E) -//val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF) -//val md_theme_dark_outline = Color(0xFF8C9199) -//val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E) -//val md_theme_dark_inverseSurface = Color(0xFFE2E2E6) -//val md_theme_dark_inversePrimary = Color(0xFF0062A1) -//val md_theme_dark_shadow = Color(0xFF000000) -//val md_theme_dark_surfaceTint = Color(0xFF9DCAFF) -//val md_theme_dark_outlineVariant = Color(0xFF42474E) -//val md_theme_dark_scrim = Color(0xFF000000) - +val md_theme_dark_primary = Color(0xFF9DCAFF) +val md_theme_dark_onPrimary = Color(0xFF003257) +val md_theme_dark_primaryContainer = Color(0xFF00497B) +val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF) +val md_theme_dark_secondary = Color(0xFFBAC8DB) +val md_theme_dark_onSecondary = Color(0xFF253140) +val md_theme_dark_secondaryContainer = Color(0xFF3B4858) +val md_theme_dark_onSecondaryContainer = Color(0xFFD6E4F7) +val md_theme_dark_tertiary = Color(0xFFB2C5FF) +val md_theme_dark_onTertiary = Color(0xFF002B73) +val md_theme_dark_tertiaryContainer = Color(0xFF1E438F) +val md_theme_dark_onTertiaryContainer = Color(0xFFDAE2FF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF1A1C1E) +val md_theme_dark_onBackground = Color(0xFFE2E2E6) +val md_theme_dark_surface = Color(0xFF1A1C1E) +val md_theme_dark_onSurface = Color(0xFFE2E2E6) +val md_theme_dark_surfaceVariant = Color(0xFF42474E) +val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF) +val md_theme_dark_outline = Color(0xFF8C9199) +val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E) +val md_theme_dark_inverseSurface = Color(0xFFE2E2E6) +val md_theme_dark_inversePrimary = Color(0xFF0062A1) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF9DCAFF) +val md_theme_dark_outlineVariant = Color(0xFF42474E) +val md_theme_dark_scrim = Color(0xFF000000) val seed = Color(0xFF109CFC) \ No newline at end of file diff --git a/app/src/main/java/com/bera/josaahelpertool/ui/theme/Theme.kt b/app/src/main/java/com/bera/josaahelpertool/ui/theme/Theme.kt index 80fc5a1..394bdda 100644 --- a/app/src/main/java/com/bera/josaahelpertool/ui/theme/Theme.kt +++ b/app/src/main/java/com/bera/josaahelpertool/ui/theme/Theme.kt @@ -48,42 +48,42 @@ private val _lightColorScheme = lightColorScheme( ) private val _darkColorScheme = darkColorScheme( -// primary = md_theme_dark_primary, -// onPrimary = md_theme_dark_onPrimary, -// primaryContainer = md_theme_dark_primaryContainer, -// onPrimaryContainer = md_theme_dark_onPrimaryContainer, -// secondary = md_theme_dark_secondary, -// onSecondary = md_theme_dark_onSecondary, -// secondaryContainer = md_theme_dark_secondaryContainer, -// onSecondaryContainer = md_theme_dark_onSecondaryContainer, -// tertiary = md_theme_dark_tertiary, -// onTertiary = md_theme_dark_onTertiary, -// tertiaryContainer = md_theme_dark_tertiaryContainer, -// onTertiaryContainer = md_theme_dark_onTertiaryContainer, -// error = md_theme_dark_error, -// errorContainer = md_theme_dark_errorContainer, -// onError = md_theme_dark_onError, -// onErrorContainer = md_theme_dark_onErrorContainer, -// background = md_theme_dark_background, -// onBackground = md_theme_dark_onBackground, -// surface = md_theme_dark_surface, -// onSurface = md_theme_dark_onSurface, -// surfaceVariant = md_theme_dark_surfaceVariant, -// onSurfaceVariant = md_theme_dark_onSurfaceVariant, -// outline = md_theme_dark_outline, -// inverseOnSurface = md_theme_dark_inverseOnSurface, -// inverseSurface = md_theme_dark_inverseSurface, -// inversePrimary = md_theme_dark_inversePrimary, -// surfaceTint = md_theme_dark_surfaceTint, -// outlineVariant = md_theme_dark_outlineVariant, -// scrim = md_theme_dark_scrim, + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, ) @Composable fun CollegeSearchTheme( - darkTheme: Boolean = false, // isSystemInDarkTheme() (to implement dark theme) + darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = false, // true (to implement dynamicColor) + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { @@ -98,9 +98,7 @@ fun CollegeSearchTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = - if (darkTheme) _darkColorScheme.surface.toArgb() else _lightColorScheme.surface.toArgb() - + window.statusBarColor = colorScheme.surface.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme } } diff --git a/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeDataStore.kt b/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeDataStore.kt new file mode 100644 index 0000000..5687b97 --- /dev/null +++ b/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeDataStore.kt @@ -0,0 +1,71 @@ +package com.bera.josaahelpertool.ui.theme + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.dataStore: DataStore by preferencesDataStore(name = "app_theme_preferences") + +enum class ThemeMode { + SYSTEM, LIGHT, DARK +} + +@Singleton +class ThemeDataStore @Inject constructor( + private val context: Context +) { + private val THEME_MODE_KEY = stringPreferencesKey("app_theme_mode") + private val DYNAMIC_COLOR_KEY = booleanPreferencesKey("app_dynamic_color") + + val themeMode: Flow = context.dataStore.data.map { preferences -> + try { + val themeModeString = preferences[THEME_MODE_KEY] + when (themeModeString) { + ThemeMode.LIGHT.name -> ThemeMode.LIGHT + ThemeMode.DARK.name -> ThemeMode.DARK + else -> ThemeMode.SYSTEM + } + } catch (e: Exception) { + // If there's any error reading the preference, default to SYSTEM + Log.e("ThemeDataStore", "Error reading theme mode preference", e) + ThemeMode.SYSTEM + } + } + + val dynamicColor: Flow = context.dataStore.data.map { preferences -> + try { + preferences[DYNAMIC_COLOR_KEY] ?: false + } catch (e: Exception) { + // If there's any error reading the preference, default to false + Log.e("ThemeDataStore", "Error reading dynamic color preference", e) + false + } + } + + suspend fun setThemeMode(themeMode: ThemeMode) { + context.dataStore.edit { preferences -> + preferences[THEME_MODE_KEY] = themeMode.name + } + } + + suspend fun setDynamicColor(enabled: Boolean) { + context.dataStore.edit { preferences -> + preferences[DYNAMIC_COLOR_KEY] = enabled + } + } + + suspend fun clearPreferences() { + context.dataStore.edit { preferences -> + preferences.clear() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeViewModel.kt b/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeViewModel.kt new file mode 100644 index 0000000..1097fc9 --- /dev/null +++ b/app/src/main/java/com/bera/josaahelpertool/ui/theme/ThemeViewModel.kt @@ -0,0 +1,77 @@ +package com.bera.josaahelpertool.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ThemeViewModel @Inject constructor( + private val themeDataStore: ThemeDataStore +) : ViewModel() { + + private val _uiState = MutableStateFlow(ThemeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + combine( + themeDataStore.themeMode, + themeDataStore.dynamicColor + ) { themeMode, dynamicColor -> + ThemeUiState( + themeMode = themeMode, + dynamicColor = dynamicColor + ) + }.collect { newState -> + _uiState.value = newState + } + } + } + + fun setThemeMode(themeMode: ThemeMode) { + viewModelScope.launch { + themeDataStore.setThemeMode(themeMode) + } + } + + fun toggleTheme() { + val currentMode = _uiState.value.themeMode + val newMode = when (currentMode) { + ThemeMode.LIGHT -> ThemeMode.DARK + ThemeMode.DARK -> ThemeMode.LIGHT + ThemeMode.SYSTEM -> ThemeMode.DARK // If system, switch to dark + } + setThemeMode(newMode) + } + + fun setDynamicColor(enabled: Boolean) { + viewModelScope.launch { + themeDataStore.setDynamicColor(enabled) + } + } + + @Composable + fun shouldUseDarkTheme(): Boolean { + val uiState by uiState.collectAsState() + return when (uiState.themeMode) { + ThemeMode.LIGHT -> false + ThemeMode.DARK -> true + ThemeMode.SYSTEM -> isSystemInDarkTheme() + } + } +} + +data class ThemeUiState( + val themeMode: ThemeMode = ThemeMode.SYSTEM, + val dynamicColor: Boolean = false +) \ No newline at end of file