diff --git a/app/src/androidTest/java/com/github/se/assocify/AppTest.kt b/app/src/androidTest/java/com/github/se/assocify/AppTest.kt index 58bdeb4c5..96b4ad008 100644 --- a/app/src/androidTest/java/com/github/se/assocify/AppTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/AppTest.kt @@ -11,7 +11,7 @@ import androidx.navigation.compose.rememberNavController import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.se.assocify.model.CurrentUser import com.github.se.assocify.model.database.UserAPI -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions import com.github.se.assocify.ui.screens.login.loginGraph @@ -36,7 +36,7 @@ import org.junit.runner.RunWith @Composable fun LoginApp() { val navController = rememberNavController() - val mockLoginSave: LoginSave = mockk(relaxed = true) + val mockLoginSave: LocalSave = mockk(relaxed = true) val navActions = NavigationActions(navController, mockLoginSave) val supabaseClient: SupabaseClient = createSupabaseClient(BuildConfig.SUPABASE_URL, BuildConfig.SUPABASE_ANON_KEY) { diff --git a/app/src/androidTest/java/com/github/se/assocify/ThemeTest.kt b/app/src/androidTest/java/com/github/se/assocify/ThemeTest.kt index 223d7d45e..c573eefaa 100644 --- a/app/src/androidTest/java/com/github/se/assocify/ThemeTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/ThemeTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onSibling import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.se.assocify.model.entities.Theme import com.github.se.assocify.ui.theme.AssocifyTheme import com.github.se.assocify.ui.theme.md_theme_dark_primary import com.github.se.assocify.ui.theme.md_theme_light_primary @@ -34,8 +35,9 @@ class ThemeTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo @Test fun darkTheme() { + composeTestRule.setContent { - AssocifyTheme(darkTheme = true, dynamicColor = false) { TestScreen() } + AssocifyTheme(theme = Theme.DARK, dynamicColor = false) { TestScreen() } } with(composeTestRule) { onNodeWithText("Test") @@ -48,7 +50,20 @@ class ThemeTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo @Test fun lightTheme() { composeTestRule.setContent { - AssocifyTheme(darkTheme = false, dynamicColor = false) { TestScreen() } + AssocifyTheme(theme = Theme.LIGHT, dynamicColor = false) { TestScreen() } + } + with(composeTestRule) { + onNodeWithText("Test") + .assertIsDisplayed() + .onSibling() + .assertTextContains(md_theme_light_primary.toString()) + } + } + + @Test + fun systemTheme() { + composeTestRule.setContent { + AssocifyTheme(theme = Theme.SYSTEM, dynamicColor = false) { TestScreen() } } with(composeTestRule) { onNodeWithText("Test") diff --git a/app/src/androidTest/java/com/github/se/assocify/epics/Epic1Test.kt b/app/src/androidTest/java/com/github/se/assocify/epics/Epic1Test.kt index 4fb22db8a..3b9995c85 100644 --- a/app/src/androidTest/java/com/github/se/assocify/epics/Epic1Test.kt +++ b/app/src/androidTest/java/com/github/se/assocify/epics/Epic1Test.kt @@ -29,10 +29,11 @@ import com.github.se.assocify.model.entities.Association import com.github.se.assocify.model.entities.PermissionRole import com.github.se.assocify.model.entities.RoleType import com.github.se.assocify.model.entities.User -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions import com.github.se.assocify.navigation.mainNavGraph +import com.github.se.assocify.ui.theme.ThemeViewModel import com.kaspersky.components.composesupport.config.withComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase @@ -98,6 +99,8 @@ class Epic1Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo } every { associationNameValid(any()) } returns true + + every { getLogo(any(), any(), any()) } answers {} } private val userAPI = @@ -147,10 +150,11 @@ class Epic1Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo private val receiptAPI = mockk(relaxUnitFun = true) - private val loginSave = mockk(relaxUnitFun = true) + private val loginSave = mockk(relaxUnitFun = true) private val accountingCategoriesAPI = mockk(relaxUnitFun = true) private val accountingSubCategoryAPI = mockk(relaxUnitFun = true) + private val appThemeViewModel = mockk(relaxUnitFun = true) @Before fun testSetup() { @@ -253,6 +257,8 @@ fun TestAssocifyApp( taskAPI = taskAPI, receiptsAPI = receiptAPI, accountingCategoriesAPI = accountingCategoriesAPI, - accountingSubCategoryAPI = accountingSubCategoryAPI) + accountingSubCategoryAPI = accountingSubCategoryAPI, + appThemeViewModel = mockk(), + localSave = mockk()) } } diff --git a/app/src/androidTest/java/com/github/se/assocify/epics/Epic2Test.kt b/app/src/androidTest/java/com/github/se/assocify/epics/Epic2Test.kt index a3ce81472..d8fdb75ce 100644 --- a/app/src/androidTest/java/com/github/se/assocify/epics/Epic2Test.kt +++ b/app/src/androidTest/java/com/github/se/assocify/epics/Epic2Test.kt @@ -30,7 +30,7 @@ import com.github.se.assocify.model.entities.Receipt import com.github.se.assocify.model.entities.RoleType import com.github.se.assocify.model.entities.Status import com.github.se.assocify.model.entities.User -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions import com.kaspersky.components.composesupport.config.withComposeSupport @@ -117,6 +117,7 @@ class Epic2Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo val onSuccessCallback = secondArg<(Association) -> Unit>() onSuccessCallback.invoke(associations.find { it.uid == assoID }!!) } + every { getLogo(any(), any(), any()) } answers {} } private val userAPI = @@ -197,7 +198,7 @@ class Epic2Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo private val taskAPI = mockk(relaxUnitFun = true) private val budgetAPI = mockk(relaxUnitFun = true) private val balanceAPI = mockk(relaxUnitFun = true) - private val loginSave = mockk(relaxUnitFun = true) + private val loginSave = mockk(relaxUnitFun = true) private val accountingCategoriesAPI = mockk(relaxUnitFun = true) private val accountingSubCategoryAPI = mockk(relaxUnitFun = true) diff --git a/app/src/androidTest/java/com/github/se/assocify/epics/Epic4Test.kt b/app/src/androidTest/java/com/github/se/assocify/epics/Epic4Test.kt index a36c98a07..66587c52e 100644 --- a/app/src/androidTest/java/com/github/se/assocify/epics/Epic4Test.kt +++ b/app/src/androidTest/java/com/github/se/assocify/epics/Epic4Test.kt @@ -27,9 +27,10 @@ import com.github.se.assocify.model.entities.Association import com.github.se.assocify.model.entities.PermissionRole import com.github.se.assocify.model.entities.RoleType import com.github.se.assocify.model.entities.User -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions +import com.github.se.assocify.ui.theme.ThemeViewModel import com.kaspersky.components.composesupport.config.withComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase @@ -105,6 +106,7 @@ class Epic4Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo } every { associationNameValid(any()) } returns true + every { getLogo(any(), any(), any()) } answers {} } private val userAPI = @@ -163,7 +165,8 @@ class Epic4Test : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSuppo private val accountingCategoryAPI = mockk(relaxUnitFun = true) - private val loginSave = mockk(relaxUnitFun = true) + private val loginSave = mockk(relaxUnitFun = true) + private val appThemeViewModel = mockk(relaxUnitFun = true) @Before fun testSetup() { diff --git a/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfilePreferencesScreenTest.kt b/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfilePreferencesScreenTest.kt index 65e52010d..7c1fbe536 100644 --- a/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfilePreferencesScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/assocify/screens/profile/ProfilePreferencesScreenTest.kt @@ -10,13 +10,17 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.se.assocify.model.CurrentUser +import com.github.se.assocify.model.entities.Theme +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.NavigationActions import com.github.se.assocify.ui.screens.profile.preferences.ProfilePreferencesScreen +import com.github.se.assocify.ui.theme.ThemeViewModel import com.kaspersky.components.composesupport.config.withComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Rule import org.junit.Test @@ -29,15 +33,21 @@ class ProfilePreferencesScreenTest : private val navActions = mockk() private var goBack = false + private lateinit var themeVM: ThemeViewModel + private val localSave = mockk() @Before fun testSetup() { CurrentUser.userUid = "1" CurrentUser.associationUid = "asso" + themeVM = mockk(relaxed = true) + every { themeVM.theme } returns MutableStateFlow(Theme.LIGHT) every { navActions.back() } answers { goBack = true } - composeTestRule.setContent { ProfilePreferencesScreen(navActions = navActions) } + composeTestRule.setContent { + ProfilePreferencesScreen(navActions = navActions, themeVM, localSave) + } } @Test @@ -47,10 +57,10 @@ class ProfilePreferencesScreenTest : onNodeWithTag("themeTitle").assertIsDisplayed() onNodeWithTag("themeSegmentedButtonRow").assertIsDisplayed() - listOf("Light", "Dark", "System").forEach { - onNodeWithText(text = it).assertIsDisplayed().assertIsSelectable() + Theme.entries.forEach { + onNodeWithText(text = it.name).assertIsDisplayed().assertIsSelectable() } - onNodeWithText(text = "Light").assertIsSelected() + onNodeWithText(text = "LIGHT").assertIsSelected() onNodeWithTag("textSize").assertIsDisplayed() onNodeWithTag("textSizeSlider").assertIsDisplayed() diff --git a/app/src/main/java/com/github/se/assocify/AssocifyApp.kt b/app/src/main/java/com/github/se/assocify/AssocifyApp.kt index f091a8193..6cf10d86c 100644 --- a/app/src/main/java/com/github/se/assocify/AssocifyApp.kt +++ b/app/src/main/java/com/github/se/assocify/AssocifyApp.kt @@ -1,67 +1,68 @@ -package com.github.se.assocify - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import com.github.se.assocify.model.CurrentUser -import com.github.se.assocify.model.SupabaseClient -import com.github.se.assocify.model.database.AccountingCategoryAPI -import com.github.se.assocify.model.database.AccountingSubCategoryAPI -import com.github.se.assocify.model.database.AssociationAPI -import com.github.se.assocify.model.database.BalanceAPI -import com.github.se.assocify.model.database.BudgetAPI -import com.github.se.assocify.model.database.EventAPI -import com.github.se.assocify.model.database.ReceiptAPI -import com.github.se.assocify.model.database.TaskAPI -import com.github.se.assocify.model.database.UserAPI -import com.github.se.assocify.model.localsave.LoginSave -import com.github.se.assocify.navigation.Destination -import com.github.se.assocify.navigation.NavigationActions -import com.github.se.assocify.navigation.mainNavGraph - -@Composable -fun AssocifyApp(loginSaver: LoginSave) { - val navController = rememberNavController() - val navActions = NavigationActions(navController, loginSaver) - - loginSaver.loadUserInfo() - - val userAPI = - UserAPI( - SupabaseClient.supabaseClient, LocalContext.current.cacheDir.toPath().resolve("users")) - val associationAPI = - AssociationAPI( - SupabaseClient.supabaseClient, - LocalContext.current.cacheDir.toPath().resolve("associations")) - val eventAPI = EventAPI(SupabaseClient.supabaseClient) - val taskAPI = TaskAPI(SupabaseClient.supabaseClient) - val receiptsAPI = - ReceiptAPI( - SupabaseClient.supabaseClient, LocalContext.current.cacheDir.toPath().resolve("receipts")) - val budgetAPI = BudgetAPI(SupabaseClient.supabaseClient) - val accountingCategoriesAPI = AccountingCategoryAPI(SupabaseClient.supabaseClient) - val accountingSubCategoryAPI = AccountingSubCategoryAPI(SupabaseClient.supabaseClient) - val balanceAPI = BalanceAPI(SupabaseClient.supabaseClient) - - val firstDest = - if (CurrentUser.userUid != null && CurrentUser.associationUid != null) { - Destination.Treasury.route - } else { - Destination.Login.route - } - - NavHost(navController = navController, startDestination = firstDest) { - mainNavGraph( - navActions = navActions, - userAPI = userAPI, - associationAPI = associationAPI, - eventAPI = eventAPI, - budgetAPI = budgetAPI, - balanceAPI = balanceAPI, - taskAPI = taskAPI, - receiptsAPI = receiptsAPI, - accountingCategoriesAPI = accountingCategoriesAPI, - accountingSubCategoryAPI = accountingSubCategoryAPI) - } -} +package com.github.se.assocify + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.github.se.assocify.model.CurrentUser +import com.github.se.assocify.model.SupabaseClient +import com.github.se.assocify.model.database.AccountingCategoryAPI +import com.github.se.assocify.model.database.AccountingSubCategoryAPI +import com.github.se.assocify.model.database.AssociationAPI +import com.github.se.assocify.model.database.BalanceAPI +import com.github.se.assocify.model.database.BudgetAPI +import com.github.se.assocify.model.database.EventAPI +import com.github.se.assocify.model.database.ReceiptAPI +import com.github.se.assocify.model.database.TaskAPI +import com.github.se.assocify.model.database.UserAPI +import com.github.se.assocify.model.localsave.LocalSave +import com.github.se.assocify.navigation.Destination +import com.github.se.assocify.navigation.NavigationActions +import com.github.se.assocify.navigation.mainNavGraph +import com.github.se.assocify.ui.theme.ThemeViewModel + +@Composable +fun AssocifyApp(localSaver: LocalSave, appThemeViewModel: ThemeViewModel) { + val navController = rememberNavController() + val navActions = NavigationActions(navController, localSaver) + + val userAPI = + UserAPI( + SupabaseClient.supabaseClient, LocalContext.current.cacheDir.toPath().resolve("users")) + val associationAPI = + AssociationAPI( + SupabaseClient.supabaseClient, + LocalContext.current.cacheDir.toPath().resolve("associations")) + val eventAPI = EventAPI(SupabaseClient.supabaseClient) + val taskAPI = TaskAPI(SupabaseClient.supabaseClient) + val receiptsAPI = + ReceiptAPI( + SupabaseClient.supabaseClient, LocalContext.current.cacheDir.toPath().resolve("receipts")) + val budgetAPI = BudgetAPI(SupabaseClient.supabaseClient) + val accountingCategoriesAPI = AccountingCategoryAPI(SupabaseClient.supabaseClient) + val accountingSubCategoryAPI = AccountingSubCategoryAPI(SupabaseClient.supabaseClient) + val balanceAPI = BalanceAPI(SupabaseClient.supabaseClient) + + val firstDest = + if (CurrentUser.userUid != null && CurrentUser.associationUid != null) { + Destination.Treasury.route + } else { + Destination.Login.route + } + + NavHost(navController = navController, startDestination = firstDest) { + mainNavGraph( + navActions = navActions, + userAPI = userAPI, + associationAPI = associationAPI, + eventAPI = eventAPI, + budgetAPI = budgetAPI, + balanceAPI = balanceAPI, + taskAPI = taskAPI, + receiptsAPI = receiptsAPI, + accountingCategoriesAPI = accountingCategoriesAPI, + accountingSubCategoryAPI = accountingSubCategoryAPI, + appThemeViewModel = appThemeViewModel, + localSave = localSaver) + } +} diff --git a/app/src/main/java/com/github/se/assocify/MainActivity.kt b/app/src/main/java/com/github/se/assocify/MainActivity.kt index 7b544895f..af6f80aed 100644 --- a/app/src/main/java/com/github/se/assocify/MainActivity.kt +++ b/app/src/main/java/com/github/se/assocify/MainActivity.kt @@ -6,19 +6,26 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.ui.theme.AssocifyTheme +import com.github.se.assocify.ui.theme.ThemeViewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) + val themeVM = ThemeViewModel() + val localSave = LocalSave(this, themeVM) + setContent { - AssocifyTheme { + val theme by themeVM.theme.collectAsState() + AssocifyTheme(theme = theme) { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - AssocifyApp(LoginSave(this)) + AssocifyApp(localSave, themeVM) } } } diff --git a/app/src/main/java/com/github/se/assocify/model/entities/UserPreference.kt b/app/src/main/java/com/github/se/assocify/model/entities/UserPreference.kt index 0e26b0a9d..ad4e44714 100644 --- a/app/src/main/java/com/github/se/assocify/model/entities/UserPreference.kt +++ b/app/src/main/java/com/github/se/assocify/model/entities/UserPreference.kt @@ -25,7 +25,17 @@ data class UserPreference( enum class Theme { LIGHT, DARK, - SYSTEM + SYSTEM; + + companion object { + fun fromString(value: String?): Theme { + return when (value) { + "LIGHT" -> LIGHT + "DARK" -> DARK + else -> SYSTEM + } + } + } } /** All the languages supported by the application */ diff --git a/app/src/main/java/com/github/se/assocify/model/localsave/LoginSave.kt b/app/src/main/java/com/github/se/assocify/model/localsave/LocalSave.kt similarity index 65% rename from app/src/main/java/com/github/se/assocify/model/localsave/LoginSave.kt rename to app/src/main/java/com/github/se/assocify/model/localsave/LocalSave.kt index 797603091..490a86dc3 100644 --- a/app/src/main/java/com/github/se/assocify/model/localsave/LoginSave.kt +++ b/app/src/main/java/com/github/se/assocify/model/localsave/LocalSave.kt @@ -1,64 +1,95 @@ -package com.github.se.assocify.model.localsave - -import android.content.Context -import android.content.SharedPreferences -import com.github.se.assocify.MainActivity -import com.github.se.assocify.model.CurrentUser - -class LoginSave(private val activity: MainActivity) { - - private val ASSOCIFY_PREF = "com.github.se.assocify.PREFERENCE_FILE_KEY" - private val USER_PREF = "user_uid" - private val ASSOC_PREF = "association_uid" - - fun saveUserInfo() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - val editor = sharedPref.edit() - editor.putString(USER_PREF, CurrentUser.userUid) - editor.putString(ASSOC_PREF, CurrentUser.associationUid) - editor.apply() - } - - fun saveAssociation() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - val editor = sharedPref.edit() - editor.putString(ASSOC_PREF, CurrentUser.associationUid) - editor.apply() - } - - fun loadUserInfo() { - loadUserUid() - loadAssociation() - } - - fun loadUserUid() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - CurrentUser.userUid = sharedPref.getString(USER_PREF, null) - } - - fun loadAssociation() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - CurrentUser.associationUid = sharedPref.getString(ASSOC_PREF, null) - } - - fun clearSavedUserInfo() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - val editor = sharedPref.edit() - editor.remove(USER_PREF) - editor.remove(ASSOC_PREF) - editor.apply() - } - - fun clearSavedAssociation() { - val sharedPref: SharedPreferences = - activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) - val editor = sharedPref.edit() - editor.remove(ASSOC_PREF) - editor.apply() - } -} +package com.github.se.assocify.model.localsave + +import android.content.Context +import android.content.SharedPreferences +import com.github.se.assocify.MainActivity +import com.github.se.assocify.model.CurrentUser +import com.github.se.assocify.model.entities.Theme +import com.github.se.assocify.ui.theme.ThemeViewModel + +class LocalSave(private val activity: MainActivity, private val themeVM: ThemeViewModel) { + + private val ASSOCIFY_PREF = "com.github.se.assocify.PREFERENCE_FILE_KEY" + private val USER_PREF = "user_uid" + private val ASSOC_PREF = "association_uid" + private val THEME_PREF = "theme" + + init { + loadUserInfo() + } + + fun saveUserInfo() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.putString(USER_PREF, CurrentUser.userUid) + editor.putString(ASSOC_PREF, CurrentUser.associationUid) + editor.putString(THEME_PREF, themeVM.theme.value.name) + editor.apply() + } + + fun saveAssociation() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.putString(ASSOC_PREF, CurrentUser.associationUid) + editor.apply() + } + + fun saveTheme() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.putString(THEME_PREF, themeVM.theme.value.name) + editor.apply() + } + + fun loadUserInfo() { + loadTheme() + loadUserUid() + loadAssociation() + } + + fun loadUserUid() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + CurrentUser.userUid = sharedPref.getString(USER_PREF, null) + } + + fun loadAssociation() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + CurrentUser.associationUid = sharedPref.getString(ASSOC_PREF, null) + } + + fun loadTheme() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + themeVM.setTheme(Theme.fromString(sharedPref.getString(THEME_PREF, null))) + } + + fun clearSavedUserInfo() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.remove(USER_PREF) + editor.remove(ASSOC_PREF) + editor.apply() + } + + fun clearSavedAssociation() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.remove(ASSOC_PREF) + editor.apply() + } + + fun clearSavedTheme() { + val sharedPref: SharedPreferences = + activity.getSharedPreferences(ASSOCIFY_PREF, Context.MODE_PRIVATE) + val editor = sharedPref.edit() + editor.remove(THEME_PREF) + editor.apply() + } +} diff --git a/app/src/main/java/com/github/se/assocify/navigation/NavigationActions.kt b/app/src/main/java/com/github/se/assocify/navigation/NavigationActions.kt index c3b785d48..4732acacc 100644 --- a/app/src/main/java/com/github/se/assocify/navigation/NavigationActions.kt +++ b/app/src/main/java/com/github/se/assocify/navigation/NavigationActions.kt @@ -4,11 +4,11 @@ import android.annotation.SuppressLint import android.util.Log import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave class NavigationActions( private val navController: NavHostController, - private val loginSaver: LoginSave + private val loginSaver: LocalSave ) { fun navigateToMainTab(destination: Destination) { if (destination in MAIN_TABS_LIST) { diff --git a/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt b/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt index 5cc8d665a..a6d58c2d9 100644 --- a/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/github/se/assocify/navigation/NavigationGraph.kt @@ -10,12 +10,14 @@ import com.github.se.assocify.model.database.EventAPI import com.github.se.assocify.model.database.ReceiptAPI import com.github.se.assocify.model.database.TaskAPI import com.github.se.assocify.model.database.UserAPI +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.ui.screens.createAssociation.createAssociationGraph import com.github.se.assocify.ui.screens.event.eventGraph import com.github.se.assocify.ui.screens.login.loginGraph import com.github.se.assocify.ui.screens.profile.profileGraph import com.github.se.assocify.ui.screens.selectAssociation.selectAssociationGraph import com.github.se.assocify.ui.screens.treasury.treasuryGraph +import com.github.se.assocify.ui.theme.ThemeViewModel fun NavGraphBuilder.mainNavGraph( navActions: NavigationActions, @@ -27,7 +29,9 @@ fun NavGraphBuilder.mainNavGraph( taskAPI: TaskAPI, receiptsAPI: ReceiptAPI, accountingCategoriesAPI: AccountingCategoryAPI, - accountingSubCategoryAPI: AccountingSubCategoryAPI + accountingSubCategoryAPI: AccountingSubCategoryAPI, + appThemeViewModel: ThemeViewModel, + localSave: LocalSave ) { treasuryGraph( navActions, @@ -38,7 +42,14 @@ fun NavGraphBuilder.mainNavGraph( accountingSubCategoryAPI, userAPI) eventGraph(navActions, eventAPI, taskAPI) - profileGraph(navActions, userAPI, associationAPI, accountingCategoriesAPI, eventAPI) + profileGraph( + navActions, + userAPI, + associationAPI, + accountingCategoriesAPI, + eventAPI, + appThemeViewModel, + localSave) loginGraph(navActions, userAPI) selectAssociationGraph(navActions, userAPI, associationAPI) createAssociationGraph(navActions, userAPI, associationAPI) diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileGraph.kt b/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileGraph.kt index df1930e34..7e9f01f4d 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileGraph.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/profile/ProfileGraph.kt @@ -7,19 +7,23 @@ import com.github.se.assocify.model.database.AccountingCategoryAPI import com.github.se.assocify.model.database.AssociationAPI import com.github.se.assocify.model.database.EventAPI import com.github.se.assocify.model.database.UserAPI +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions import com.github.se.assocify.ui.screens.profile.events.profileEventsGraph import com.github.se.assocify.ui.screens.profile.members.profileMembersGraph import com.github.se.assocify.ui.screens.profile.preferences.profilePreferencesGraph import com.github.se.assocify.ui.screens.profile.treasuryTags.profileTreasuryTagsGraph +import com.github.se.assocify.ui.theme.ThemeViewModel fun NavGraphBuilder.profileGraph( navigationActions: NavigationActions, userAPI: UserAPI, associationAPI: AssociationAPI, accountingCategoryAPI: AccountingCategoryAPI, - eventAPI: EventAPI + eventAPI: EventAPI, + appThemeVM: ThemeViewModel, + localSave: LocalSave ) { composable( route = Destination.Profile.route, @@ -28,7 +32,7 @@ fun NavGraphBuilder.profileGraph( ProfileScreen(navigationActions, profileViewModel) } - profilePreferencesGraph(navigationActions) + profilePreferencesGraph(navigationActions, appThemeVM, localSave) profileMembersGraph(navigationActions, associationAPI) profileTreasuryTagsGraph(navigationActions, accountingCategoryAPI) profileEventsGraph(navigationActions, eventAPI) diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesGraph.kt b/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesGraph.kt index 3698e3d17..f21722dc8 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesGraph.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesGraph.kt @@ -2,11 +2,18 @@ package com.github.se.assocify.ui.screens.profile.preferences import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions +import com.github.se.assocify.ui.theme.ThemeViewModel -fun NavGraphBuilder.profilePreferencesGraph(navigationActions: NavigationActions) { +fun NavGraphBuilder.profilePreferencesGraph( + navigationActions: NavigationActions, + appThemeVM: ThemeViewModel, + localSave: LocalSave +) { composable(route = Destination.ProfilePreferences.route) { - ProfilePreferencesScreen(navActions = navigationActions) + ProfilePreferencesScreen( + navActions = navigationActions, appThemeViewModel = appThemeVM, localSave = localSave) } } diff --git a/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesScreen.kt b/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesScreen.kt index 1fee06fbf..d3a1c5969 100644 --- a/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesScreen.kt +++ b/app/src/main/java/com/github/se/assocify/ui/screens/profile/preferences/ProfilePreferencesScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -30,9 +31,12 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import com.github.se.assocify.model.entities.Theme +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.NavigationActions import com.github.se.assocify.ui.composables.DropdownOption import com.github.se.assocify.ui.composables.DropdownWithSetOptions +import com.github.se.assocify.ui.theme.ThemeViewModel /** * The screen for the user to change the theme, text size, language and currency Accessed from the @@ -44,10 +48,16 @@ import com.github.se.assocify.ui.composables.DropdownWithSetOptions */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ProfilePreferencesScreen(navActions: NavigationActions) { +fun ProfilePreferencesScreen( + navActions: NavigationActions, + appThemeViewModel: ThemeViewModel, + localSave: LocalSave +) { // temporary values for the theme, text size, language and currency, waiting for the viewmodel - val themeOptions = listOf("Light", "Dark", "System") - var themeSelectedIndex by remember { mutableIntStateOf(0) } + val themeOptions = listOf(Theme.LIGHT, Theme.DARK, Theme.SYSTEM) + val currentTheme by appThemeViewModel.theme.collectAsState() + + var themeSelectedIndex by remember { mutableIntStateOf(themeOptions.indexOf(currentTheme)) } var sliderPosition by remember { mutableFloatStateOf(15f) } @@ -84,11 +94,16 @@ fun ProfilePreferencesScreen(navActions: NavigationActions) { shape = SegmentedButtonDefaults.itemShape( index = index, count = themeOptions.size), - onClick = { themeSelectedIndex = index }, - selected = index == themeSelectedIndex, - ) { - Text(label) - } + onClick = { + if (index != themeSelectedIndex) { + themeSelectedIndex = index + appThemeViewModel.setTheme(label) + localSave.saveTheme() + } + }, + selected = index == themeSelectedIndex) { + Text(label.name) + } } } diff --git a/app/src/main/java/com/github/se/assocify/ui/theme/Theme.kt b/app/src/main/java/com/github/se/assocify/ui/theme/Theme.kt index f908201f2..75a2db20f 100644 --- a/app/src/main/java/com/github/se/assocify/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/se/assocify/ui/theme/Theme.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.github.se.assocify.model.entities.Theme private val LightColorScheme = lightColorScheme( @@ -83,18 +84,26 @@ private val DarkColorScheme = @Composable fun AssocifyTheme( - darkTheme: Boolean = isSystemInDarkTheme(), + theme: Theme, // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { + val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + when (theme) { + Theme.LIGHT -> dynamicLightColorScheme(context) + Theme.DARK -> dynamicDarkColorScheme(context) + Theme.SYSTEM -> + if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) + else dynamicLightColorScheme(context) + } } - darkTheme -> DarkColorScheme + theme == Theme.DARK -> DarkColorScheme + theme == Theme.SYSTEM -> if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme else -> LightColorScheme } val view = LocalView.current @@ -102,7 +111,8 @@ fun AssocifyTheme( SideEffect { val window = (view.context as Activity).window window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = + theme == Theme.DARK } } diff --git a/app/src/main/java/com/github/se/assocify/ui/theme/ThemeViewModel.kt b/app/src/main/java/com/github/se/assocify/ui/theme/ThemeViewModel.kt new file mode 100644 index 000000000..d6c5302e2 --- /dev/null +++ b/app/src/main/java/com/github/se/assocify/ui/theme/ThemeViewModel.kt @@ -0,0 +1,16 @@ +package com.github.se.assocify.ui.theme + +import com.github.se.assocify.model.entities.Theme +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ThemeViewModel { + private val _theme: MutableStateFlow = MutableStateFlow(Theme.SYSTEM) + val theme: StateFlow = _theme + + fun setTheme(theme: Theme) { + if (_theme.value != theme) { + _theme.value = theme + } + } +} diff --git a/app/src/test/java/com/github/se/assocify/ThemeViewModelTest.kt b/app/src/test/java/com/github/se/assocify/ThemeViewModelTest.kt new file mode 100644 index 000000000..5a11b9841 --- /dev/null +++ b/app/src/test/java/com/github/se/assocify/ThemeViewModelTest.kt @@ -0,0 +1,19 @@ +package com.github.se.assocify + +import com.github.se.assocify.model.entities.Theme +import com.github.se.assocify.ui.theme.ThemeViewModel +import org.junit.Test + +class ThemeViewModelTest { + + @Test + fun testSetTheme() { + val themeVM = ThemeViewModel() + themeVM.setTheme(Theme.DARK) + assert(themeVM.theme.value == Theme.DARK) + themeVM.setTheme(Theme.LIGHT) + assert(themeVM.theme.value == Theme.LIGHT) + themeVM.setTheme(Theme.SYSTEM) + assert(themeVM.theme.value == Theme.SYSTEM) + } +} diff --git a/app/src/test/java/com/github/se/assocify/model/localsave/LoginSaveTest.kt b/app/src/test/java/com/github/se/assocify/model/localsave/LoginSaveTest.kt index 411f0b568..584d7a9c6 100644 --- a/app/src/test/java/com/github/se/assocify/model/localsave/LoginSaveTest.kt +++ b/app/src/test/java/com/github/se/assocify/model/localsave/LoginSaveTest.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.SharedPreferences import com.github.se.assocify.MainActivity import com.github.se.assocify.model.CurrentUser +import com.github.se.assocify.model.entities.Theme +import com.github.se.assocify.ui.theme.ThemeViewModel import io.mockk.every import io.mockk.junit4.MockKRule import io.mockk.junit5.MockKExtension @@ -16,6 +18,11 @@ import org.junit.Test class LoginSaveTest { @get:Rule val mockkRule = MockKRule(this) + var appThemeVm = + mockk(relaxed = true) { + every { theme.value } returns Theme.DARK + every { theme.value.name } returns "DARK" + } var user: String? = null var assoc: String? = null @@ -32,6 +39,11 @@ class LoginSaveTest { assoc = secondArg() this@mockk } + every { putString("theme", any()) } answers + { + appThemeVm = mockk { every { theme.value.name } returns secondArg() } + this@mockk + } every { remove("user_uid") } answers { @@ -44,6 +56,11 @@ class LoginSaveTest { assoc = null this@mockk } + every { remove("theme") } answers + { + appThemeVm = mockk { every { theme.value } returns Theme.SYSTEM } + this@mockk + } every { apply() } answers {} } @@ -54,6 +71,8 @@ class LoginSaveTest { every { getString("user_uid", null) } answers { user } every { getString("association_uid", null) } answers { assoc } + + every { getString("theme", null) } answers { "DARK" } } val activity: MainActivity = @@ -63,7 +82,7 @@ class LoginSaveTest { } answers { prefs } } - val loginSaver = LoginSave(activity) + val loginSaver = LocalSave(activity, themeVM = appThemeVm) @Before fun setup() { @@ -76,6 +95,7 @@ class LoginSaveTest { loginSaver.saveUserInfo() assert(user == "testUser") assert(assoc == "testAssociation") + assert(appThemeVm.theme.value.name == Theme.DARK.name) } @Test @@ -111,4 +131,24 @@ class LoginSaveTest { assert(user == "newUser") assert(assoc == null) } + + @Test + fun testSaveTheme() { + loginSaver.saveTheme() + assert(appThemeVm.theme.value.name == Theme.DARK.name) + } + + @Test + fun testLoadTheme() { + appThemeVm = mockk { every { theme.value } returns Theme.LIGHT } + loginSaver.loadTheme() + assert(appThemeVm.theme.value == Theme.LIGHT) + } + + @Test + fun testClearSavedTheme() { + appThemeVm = mockk { every { theme.value } returns Theme.LIGHT } + loginSaver.clearSavedTheme() + assert(appThemeVm.theme.value == Theme.SYSTEM) + } } diff --git a/app/src/test/java/com/github/se/assocify/navigation/NavigationActionsTest.kt b/app/src/test/java/com/github/se/assocify/navigation/NavigationActionsTest.kt index c9f8d0162..2af2ac130 100644 --- a/app/src/test/java/com/github/se/assocify/navigation/NavigationActionsTest.kt +++ b/app/src/test/java/com/github/se/assocify/navigation/NavigationActionsTest.kt @@ -1,7 +1,7 @@ import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import com.github.se.assocify.model.CurrentUser -import com.github.se.assocify.model.localsave.LoginSave +import com.github.se.assocify.model.localsave.LocalSave import com.github.se.assocify.navigation.Destination import com.github.se.assocify.navigation.NavigationActions import io.mockk.mockk @@ -15,7 +15,7 @@ import org.junit.runners.JUnit4 class NavigationActionsTest { private lateinit var navController: NavHostController - private lateinit var loginSave: LoginSave + private lateinit var loginSave: LocalSave private lateinit var navigationActions: NavigationActions @Before