Merge commit '629137249e'
				
					
				
			This commit is contained in:
		
						commit
						43838f5d6d
					
				
					 84 changed files with 3324 additions and 524 deletions
				
			
		
							
								
								
									
										2
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| <project version="4"> | ||||
|   <component name="ExternalStorageConfigurationManager" enabled="true" /> | ||||
|   <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> | ||||
|   <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> | ||||
|     <output url="file://$PROJECT_DIR$/build/classes" /> | ||||
|   </component> | ||||
|   <component name="ProjectType"> | ||||
|  |  | |||
							
								
								
									
										74
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/FabTest.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.material.FloatingActionButton | ||||
| import androidx.compose.ui.test.hasClickAction | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.AddButtonActions | ||||
| import be.ugent.sel.studeez.common.composable.ExpandedAddButton | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class FabTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun expandFabTest() { | ||||
|         var expand = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             FloatingActionButton( | ||||
|                 onClick = {expand = true} | ||||
|             ) {} | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNode(hasClickAction()) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(expand) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun fabTest() { | ||||
|         var task = false | ||||
|         var session = false | ||||
|         var friend = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             ExpandedAddButton( | ||||
|                 addButtonActions = AddButtonActions( | ||||
|                     {task = true}, | ||||
|                     {friend = true}, | ||||
|                     {session = true} | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription("Session") | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription("Task") | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription("Friend") | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(task) | ||||
|         assert(session) | ||||
|         assert(friend) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										207
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/HomeScreenTest.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,207 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onAllNodesWithContentDescription | ||||
| import androidx.compose.ui.test.onAllNodesWithText | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.feed.FeedUiState | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.screens.home.HomeScreen | ||||
| import org.junit.Assert | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class HomeScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun homeScreenTest() { | ||||
|         var continueTask = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             HomeScreen( | ||||
|                 drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|                 navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}), | ||||
|                 feedUiState = FeedUiState.Succes(mapOf( | ||||
|                     "08 May 2023" to listOf( | ||||
|                         FeedEntry( | ||||
|                             argb_color = 0xFFABD200, | ||||
|                             subJectName = "Test Subject", | ||||
|                             taskName = "Test Task", | ||||
|                             totalStudyTime = 600, | ||||
|                         ) | ||||
|                     ) | ||||
|                 )), | ||||
|                 continueTask = {_, _ -> continueTask = true }, | ||||
|                 onEmptyFeedHelp = {}, | ||||
|                 onViewFriendsClick = {}, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "continue", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         Assert.assertTrue(continueTask) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun drawerTest() { | ||||
|         var homebuttontest = false | ||||
|         var timersbuttontest = false | ||||
|         var settingsbuttontest = false | ||||
|         var logoutbuttontest = false | ||||
|         var aboutbuttontest = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             HomeScreen( | ||||
|                 drawerActions = DrawerActions( | ||||
|                     {homebuttontest = true}, | ||||
|                     {timersbuttontest = true}, | ||||
|                     {settingsbuttontest = true}, | ||||
|                     {logoutbuttontest = true}, | ||||
|                     {aboutbuttontest = true} | ||||
|                 ), | ||||
|                 navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}), | ||||
|                 feedUiState = FeedUiState.Succes(mapOf()), | ||||
|                 continueTask = {_, _ -> }, | ||||
|                 onEmptyFeedHelp = {}, | ||||
|                 onViewFriendsClick = {}, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onAllNodesWithText( | ||||
|                 "home", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             )[2] // Third node has the button | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "timer", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "settings", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "log out", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "about", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         Assert.assertTrue(homebuttontest) | ||||
|         Assert.assertTrue(timersbuttontest) | ||||
|         Assert.assertTrue(settingsbuttontest) | ||||
|         Assert.assertTrue(logoutbuttontest) | ||||
|         Assert.assertTrue(aboutbuttontest) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun navigationbarTest() { | ||||
|         var hometest = false | ||||
|         var tasktest = false | ||||
|         var sessiontest = false | ||||
|         var profiletest = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             HomeScreen( | ||||
|                 drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|                 navigationBarActions = NavigationBarActions( | ||||
|                     {false}, | ||||
|                     {hometest = true}, | ||||
|                     {tasktest = true}, | ||||
|                     {sessiontest = true}, | ||||
|                     {profiletest = true}, | ||||
|                     {}, {}, {} | ||||
|                 ), | ||||
|                 feedUiState = FeedUiState.Succes(mapOf()), | ||||
|                 continueTask = {_, _ -> }, | ||||
|                 onEmptyFeedHelp = {}, | ||||
|                 onViewFriendsClick = {}, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onAllNodesWithContentDescription( | ||||
|                 "Home", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             )[0] // Third node has the button | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 "tasks", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 "session", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 "profile", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         Assert.assertTrue(hometest) | ||||
|         Assert.assertTrue(tasktest) | ||||
|         Assert.assertTrue(sessiontest) | ||||
|         Assert.assertTrue(profiletest) | ||||
|     } | ||||
| } | ||||
|  | @ -14,7 +14,7 @@ import org.junit.Assert.* | |||
|  * See [testing documentation](http://d.android.com/tools/testing). | ||||
|  */ | ||||
| @RunWith(AndroidJUnit4::class) | ||||
| class ExampleInstrumentedTest { | ||||
| class InstrumentedTest { | ||||
|     @Test | ||||
|     fun useAppContext() { | ||||
|         // Context of the app under test. | ||||
|  | @ -0,0 +1,68 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onAllNodesWithText | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.screens.log_in.LoginScreen | ||||
| import be.ugent.sel.studeez.screens.log_in.LoginScreenActions | ||||
| import be.ugent.sel.studeez.screens.log_in.LoginUiState | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class LoginScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun loginScreenTest() { | ||||
|         var login = false | ||||
|         var signup = false | ||||
|         var forgot_password = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             LoginScreen( | ||||
|                 uiState = LoginUiState(), | ||||
|                 loginScreenActions = LoginScreenActions( | ||||
|                     {}, {}, | ||||
|                     {signup = true}, | ||||
|                     {login = true}, | ||||
|                     {forgot_password = true} | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onAllNodesWithText( | ||||
|                 text = "Sign in", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             )[0] // The first object is the button | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "Forgot", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "Sign up", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(signup) | ||||
|         assert(login) | ||||
|         assert(forgot_password) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,70 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.assert | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileActions | ||||
| import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileScreen | ||||
| import be.ugent.sel.studeez.screens.profile.edit_profile.ProfileEditUiState | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class ProfileEditScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun profileEditScreenTest() { | ||||
|         var edit_save = false | ||||
|         var goback = false | ||||
|         var delete_click = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             EditProfileScreen( | ||||
|                 goBack = {goback = true}, | ||||
|                 uiState = ProfileEditUiState(), | ||||
|                 editProfileActions = EditProfileActions( | ||||
|                     onUserNameChange = {}, | ||||
|                     onBiographyChange = {}, | ||||
|                     onSaveClick = {edit_save = true}, | ||||
|                     onDeleteClick = { delete_click = true }, | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "save", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "delete", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 label = "go back", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(edit_save) | ||||
|         assert(goback) | ||||
|         assert(delete_click) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.screens.profile.ProfileActions | ||||
| import be.ugent.sel.studeez.screens.profile.ProfileScreen | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class ProfileScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun profileScreenTest() { | ||||
|         var edit = false | ||||
|         var view_friends = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             ProfileScreen( | ||||
|                 profileActions = ProfileActions( | ||||
|                     getUsername = {null}, | ||||
|                     onEditProfileClick = {edit = true}, | ||||
|                     getBiography = {null}, | ||||
|                     getAmountOfFriends = { flowOf(0) }, | ||||
|                     onViewFriendsClick = {view_friends = true} | ||||
|                 ), | ||||
|                 drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|                 navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 label = "edit profile", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "friends", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true, | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(edit) | ||||
|         assert(view_friends) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,75 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapActions | ||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapScreen | ||||
| import com.google.firebase.Timestamp | ||||
| import org.junit.Assert | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| 
 | ||||
| class SessionRecapScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun sessionRecapTest() { | ||||
|         var saveCalled = false | ||||
|         var discardCalled = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SessionRecapScreen( | ||||
|                 Modifier, | ||||
|                 SessionRecapActions( | ||||
|                     { | ||||
|                         SessionReport( | ||||
|                             "", | ||||
|                             0, | ||||
|                             Timestamp(0, 0), | ||||
|                             "") | ||||
|                     }, | ||||
|                     { saveCalled = true }, | ||||
|                     { discardCalled = true } | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "You studied", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "save", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "discard", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         Assert.assertTrue(saveCalled) | ||||
|         Assert.assertTrue(discardCalled) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onAllNodesWithText | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpActions | ||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpScreen | ||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpUiState | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class SignUpScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun signupScreenTest() { | ||||
|         var create = false | ||||
|         var login = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SignUpScreen( | ||||
|                 uiState = SignUpUiState(), | ||||
|                 signUpActions = SignUpActions({}, {}, {}, {}, {create = true}, {login = true}) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "log in", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onAllNodesWithText( | ||||
|                 text = "Create account", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             )[0] // First node has the button | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(login) | ||||
|         assert(create) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,40 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onAllNodesWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.screens.splash.SplashScreen | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class SplashScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun splashScreenTest() { | ||||
|         var tryAgain = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SplashScreen( | ||||
|                 Modifier, | ||||
|                 {tryAgain = true}, | ||||
|                 true | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onAllNodesWithText( | ||||
|                 text = "try again", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             )[1] // Second node is the button | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(tryAgain) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,158 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.screens.subjects.SubjectScreen | ||||
| import be.ugent.sel.studeez.screens.subjects.SubjectUiState | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectForm | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectFormUiState | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class SubjectScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun addSubjectScreenTest() { | ||||
|         var confirm = false | ||||
|         var goback = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SubjectForm( | ||||
|                 title = R.string.new_subject, | ||||
|                 goBack = {goback = true}, | ||||
|                 uiState = SubjectFormUiState(), | ||||
|                 onConfirm = {confirm = true}, | ||||
|                 onNameChange = {}, | ||||
|                 onColorChange = {}, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "confirm", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 label = "go back", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(confirm) | ||||
|         assert(goback) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun editSubjectScreenTest() { | ||||
|         var confirm = false | ||||
|         var delete = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SubjectForm( | ||||
|                 title = R.string.edit_subject, | ||||
|                 goBack = {}, | ||||
|                 uiState = SubjectFormUiState( | ||||
|                     name = "Test Subject", | ||||
|                 ), | ||||
|                 onConfirm = {confirm = true}, | ||||
|                 onNameChange = {}, | ||||
|                 onColorChange = {}, | ||||
|             ) | ||||
|             DeleteButton(text = R.string.delete_subject) { | ||||
|                 delete = true | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "confirm", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "delete", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(confirm) | ||||
|         assert(delete) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun subjectScreenTest() { | ||||
|         var view = false | ||||
|         var add = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             SubjectScreen( | ||||
|                 drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|                 navigationBarActions = NavigationBarActions({false}, {}, {}, {}, {}, {}, {}, {}), | ||||
|                 onAddSubject = { add = true }, | ||||
|                 onViewSubject = { view = true }, | ||||
|                 getStudyTime = { flowOf() }, | ||||
|                 getCompletedTaskCount = { flowOf() }, | ||||
|                 getTaskCount = { flowOf() }, | ||||
|                 uiState = SubjectUiState.Succes( | ||||
|                     listOf( | ||||
|                         Subject( | ||||
|                             id = "", | ||||
|                             name = "Test Subject", | ||||
|                             argb_color = 0xFFFFD200, | ||||
|                             archived = false | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "view", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "new subject", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(add) | ||||
|         assert(view) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										160
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								app/src/androidTest/java/be/ugent/sel/studeez/TaskScreenTest.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,160 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithContentDescription | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.screens.tasks.TaskActions | ||||
| import be.ugent.sel.studeez.screens.tasks.TaskScreen | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskForm | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskFormUiState | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class TaskScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun addTaskScreenTest() { | ||||
|         var confirm = false | ||||
|         var goback = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             TaskForm( | ||||
|                 title = R.string.new_task, | ||||
|                 goBack = {goback = true}, | ||||
|                 uiState = TaskFormUiState(), | ||||
|                 onConfirm = {confirm = true}, | ||||
|                 onNameChange = {}, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "confirm", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 label = "go back", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(confirm) | ||||
|         assert(goback) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun editTaskScreenTest() { | ||||
|         var confirm = false | ||||
|         var delete = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             TaskForm( | ||||
|                 title = R.string.edit_task, | ||||
|                 goBack = {}, | ||||
|                 uiState = TaskFormUiState( | ||||
|                     name = "Test Task", | ||||
|                 ), | ||||
|                 onConfirm = {confirm = true}, | ||||
|                 onNameChange = {}, | ||||
|             ) { | ||||
|                 DeleteButton(text = R.string.delete_task) { | ||||
|                     delete = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "confirm", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "delete", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(confirm) | ||||
|         assert(delete) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun taskScreenTest() { | ||||
|         var add = false | ||||
|         var edit = false | ||||
|         var start = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             TaskScreen( | ||||
|                 goBack = {}, | ||||
|                 taskActions = TaskActions( | ||||
|                     {add = true}, | ||||
|                     { Subject(name = "Test Subject") }, | ||||
|                     { flowOf(listOf(Task())) }, | ||||
|                     { _, _ -> run {} }, | ||||
|                     {edit = true}, | ||||
|                     {start = true}, | ||||
|                     {}, | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithContentDescription( | ||||
|                 label = "edit", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "new", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "start", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(add) | ||||
|         assert(edit) | ||||
|         assert(start) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,58 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.EndlessTimerInfo | ||||
| import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewActions | ||||
| import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewScreen | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class TimerOverviewScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun timerOverviewScreenTest() { | ||||
|         var add = false | ||||
|         var edit = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             TimerOverviewScreen( | ||||
|                 timerOverviewActions = TimerOverviewActions( | ||||
|                     { flowOf(listOf(EndlessTimerInfo("", ""))) }, | ||||
|                     { listOf() }, | ||||
|                     {edit = true}, | ||||
|                     {add = true} | ||||
|                 ), | ||||
|                 drawerActions = DrawerActions({}, {}, {}, {}, {}) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "add", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "edit", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(add) | ||||
|         assert(edit) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,40 @@ | |||
| package be.ugent.sel.studeez | ||||
| 
 | ||||
| import androidx.compose.ui.test.junit4.createComposeRule | ||||
| import androidx.compose.ui.test.onNodeWithText | ||||
| import androidx.compose.ui.test.performClick | ||||
| import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionActions | ||||
| import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionScreen | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class TimerSelectionScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun timerOverviewScreenTest() { | ||||
|         var start = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             TimerSelectionScreen( | ||||
|                 timerSelectionActions = TimerSelectionActions({ flowOf()}, {start = true}, 0), | ||||
|                 popUp = {} | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "start", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(start) | ||||
|     } | ||||
| } | ||||
|  | @ -33,7 +33,11 @@ import be.ugent.sel.studeez.common.ext.defaultButtonShape | |||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | ||||
| fun BasicTextButton( | ||||
|     @StringRes text: Int, | ||||
|     modifier: Modifier, | ||||
|     action: () -> Unit | ||||
| ) { | ||||
|     TextButton( | ||||
|         onClick = action, | ||||
|         modifier = modifier | ||||
|  |  | |||
|  | @ -0,0 +1,44 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| 
 | ||||
| @Composable | ||||
| fun ProfilePicture() { | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .size(40.dp) | ||||
|             .background(MaterialTheme.colors.primary, CircleShape) | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = Icons.Default.Person, | ||||
|             contentDescription = stringResource(id = R.string.username), | ||||
|             modifier = Modifier | ||||
|                 .size(30.dp) | ||||
|                 .align(Alignment.Center), | ||||
|             tint = MaterialTheme.colors.onPrimary | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun ProfilePicturePreview() { | ||||
|     StudeezTheme { | ||||
|         ProfilePicture() | ||||
|     } | ||||
| } | ||||
|  | @ -1,15 +1,16 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.text.KeyboardActions | ||||
| import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Email | ||||
| import androidx.compose.material.icons.filled.Lock | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.material.icons.filled.Search | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.painterResource | ||||
|  | @ -22,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview | |||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | ||||
| import be.ugent.sel.studeez.resources | ||||
| import com.google.android.material.color.MaterialColors | ||||
| import kotlin.math.sin | ||||
| import be.ugent.sel.studeez.R.drawable as AppIcon | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
|  | @ -47,7 +49,7 @@ fun LabelledInputField( | |||
|     value: String, | ||||
|     onNewValue: (String) -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     singleLine: Boolean = false | ||||
|     singleLine: Boolean = true | ||||
| ) { | ||||
|     OutlinedTextField( | ||||
|         value = value, | ||||
|  | @ -119,7 +121,9 @@ fun LabeledErrorTextField( | |||
|     initialValue: String, | ||||
|     @StringRes label: Int, | ||||
|     singleLine: Boolean = false, | ||||
|     errorText: Int, | ||||
|     isValid: MutableState<Boolean> = remember { mutableStateOf(true) }, | ||||
|     isFirst: MutableState<Boolean> = remember { mutableStateOf(false) }, | ||||
|     @StringRes errorText: Int, | ||||
|     keyboardType: KeyboardType, | ||||
|     predicate: (String) -> Boolean, | ||||
|     onNewCorrectValue: (String) -> Unit | ||||
|  | @ -128,31 +132,28 @@ fun LabeledErrorTextField( | |||
|         mutableStateOf(initialValue) | ||||
|     } | ||||
| 
 | ||||
|     var isValid by remember { | ||||
|         mutableStateOf(predicate(value)) | ||||
|     } | ||||
| 
 | ||||
|     Column { | ||||
|         OutlinedTextField( | ||||
|             modifier = modifier.fieldModifier(), | ||||
|             value = value, | ||||
|             onValueChange = { newText -> | ||||
|                 isFirst.value = false | ||||
|                 value = newText | ||||
|                 isValid = predicate(value) | ||||
|                 if (isValid) { | ||||
|                 isValid.value = predicate(value) | ||||
|                 if (isValid.value) { | ||||
|                     onNewCorrectValue(newText) | ||||
|                 } | ||||
|             }, | ||||
|             singleLine = singleLine, | ||||
|             label = { Text(text = stringResource(id = label)) }, | ||||
|             isError = !isValid, | ||||
|             isError = !isValid.value && !isFirst.value, | ||||
|             keyboardOptions = KeyboardOptions( | ||||
|                 keyboardType = keyboardType, | ||||
|                 imeAction = ImeAction.Done | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         if (!isValid) { | ||||
|         if (!isValid.value && !isFirst.value) { | ||||
|             Text( | ||||
|                 modifier = Modifier.padding(start = 16.dp), | ||||
|                 text = stringResource(id = errorText), | ||||
|  | @ -219,3 +220,33 @@ private fun PasswordField( | |||
|         visualTransformation = visualTransformation | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SearchField( | ||||
|     value: String, | ||||
|     onValueChange: (String) -> Unit, | ||||
|     onSubmit: () -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     modifier: Modifier = Modifier | ||||
| ) { | ||||
|     OutlinedTextField( | ||||
|         value = value, | ||||
|         onValueChange = onValueChange, | ||||
|         modifier = modifier, | ||||
|         label = { Text(text = stringResource(id = label)) }, | ||||
|         trailingIcon = { | ||||
|             IconButton(onClick = onSubmit) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Default.Search, | ||||
|                     contentDescription = stringResource(label), | ||||
|                     tint = MaterialTheme.colors.primary | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|         singleLine = true, | ||||
|         colors = TextFieldDefaults.outlinedTextFieldColors( | ||||
|             textColor = MaterialTheme.colors.onBackground, | ||||
|             backgroundColor = MaterialTheme.colors.background | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | @ -5,6 +5,7 @@ import be.ugent.sel.studeez.data.local.models.task.Subject | |||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
|  | @ -43,3 +44,10 @@ class SelectedSubject @Inject constructor() : SelectedState<Subject>() { | |||
| class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() { | ||||
|     override lateinit var value: TimerInfo | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| class SelectedUserId @Inject constructor( | ||||
|     userDAO: UserDAO | ||||
| ): SelectedState<String>() { | ||||
|     override var value: String = userDAO.getCurrentUserId() | ||||
| } | ||||
|  | @ -0,0 +1,11 @@ | |||
| package be.ugent.sel.studeez.data.local.models | ||||
| 
 | ||||
| import com.google.firebase.Timestamp | ||||
| import com.google.firebase.firestore.DocumentId | ||||
| 
 | ||||
| data class Friendship( | ||||
|     @DocumentId val id: String = "", | ||||
|     val friendId: String = "", | ||||
|     val friendsSince: Timestamp = Timestamp.now(), | ||||
|     val accepted: Boolean = false | ||||
| ) | ||||
|  | @ -1,3 +1,9 @@ | |||
| package be.ugent.sel.studeez.data.local.models | ||||
| 
 | ||||
| data class User(val id: String = "") | ||||
| import com.google.firebase.firestore.DocumentId | ||||
| 
 | ||||
| data class User( | ||||
|     @DocumentId val id: String = "", | ||||
|     val username: String = "", | ||||
|     val biography: String = "" | ||||
| ) | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package be.ugent.sel.studeez.data.local.models.task | ||||
| 
 | ||||
| import com.google.firebase.firestore.DocumentId | ||||
| import com.google.firebase.firestore.Exclude | ||||
| 
 | ||||
| data class Subject( | ||||
|     @DocumentId val id: String = "", | ||||
|  |  | |||
|  | @ -0,0 +1,7 @@ | |||
| package be.ugent.sel.studeez.data.remote | ||||
| 
 | ||||
| object FirebaseFriendship { | ||||
|     const val FRIENDID: String = "friendId" | ||||
|     const val ACCEPTED: String = "accepted" | ||||
|     const val FRIENDSSINCE: String = "friendsSince" | ||||
| } | ||||
|  | @ -0,0 +1,6 @@ | |||
| package be.ugent.sel.studeez.data.remote | ||||
| 
 | ||||
| object FirebaseSessionReport { | ||||
|     const val STUDYTIME: String = "studyTime" | ||||
|     const val ENDTIME: String = "endTime" | ||||
| } | ||||
|  | @ -0,0 +1,6 @@ | |||
| package be.ugent.sel.studeez.data.remote | ||||
| 
 | ||||
| object FirebaseUser { | ||||
|     const val USERNAME: String = "username" | ||||
|     const val BIOGRAPHY: String = "biography" | ||||
| } | ||||
|  | @ -16,6 +16,9 @@ abstract class DatabaseModule { | |||
|     @Binds | ||||
|     abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO | ||||
| 
 | ||||
|  | @ -26,13 +29,13 @@ abstract class DatabaseModule { | |||
|     abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO | ||||
|     abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO | ||||
|     abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO | ||||
|     abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO | ||||
|  |  | |||
|  | @ -0,0 +1,54 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.Friendship | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| 
 | ||||
| /** | ||||
|  * Should be used for interactions between friends. | ||||
|  */ | ||||
| interface FriendshipDAO { | ||||
| 
 | ||||
|     /** | ||||
|      * @return all friendships of a chosen user. | ||||
|      */ | ||||
|     fun getAllFriendships( | ||||
|         userId: String | ||||
|     ): Flow<List<Friendship>> | ||||
| 
 | ||||
|     /** | ||||
|      * @return the amount of friends of a chosen user. | ||||
|      * This method should be faster than just counting the length of getAllFriends() | ||||
|      */ | ||||
|     fun getFriendshipCount( | ||||
|         userId: String | ||||
|     ): Flow<Int> | ||||
| 
 | ||||
|     /** | ||||
|      * @param id the id of the friendship that you want details of | ||||
|      * @return the details of a Friendship | ||||
|      */ | ||||
|     fun getFriendshipDetails(id: String): Friendship | ||||
| 
 | ||||
|     /** | ||||
|      * Send a friend request to a user. | ||||
|      * @param id of the user that you want to add as a friend | ||||
|      * @return Success/faillure of transaction | ||||
|      */ | ||||
|     fun sendFriendshipRequest(id: String): Boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Accept a friend request that has already been sent. | ||||
|      * @param id of the friendship that you want to update | ||||
|      * @return: Success/faillure of transaction | ||||
|      */ | ||||
|     fun acceptFriendship(id: String): Boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Remove a friend or decline a friendrequest. | ||||
|      * @param friendship the one you want to remove | ||||
|      * @return: Success/faillure of transaction | ||||
|      */ | ||||
|     fun removeFriendship( | ||||
|         friendship: Friendship | ||||
|     ): Boolean | ||||
| } | ||||
|  | @ -1,12 +1,19 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| 
 | ||||
| interface SessionDAO { | ||||
| 
 | ||||
|     fun getSessions(): Flow<List<SessionReport>> | ||||
|     suspend fun getSessionsOfUser(userId: String): List<SessionReport> | ||||
| 
 | ||||
|     /** | ||||
|      * Return a list of pairs, containing the username and all the studysessions of that user. | ||||
|      */ | ||||
|     fun getFriendsSessions(): Flow<List<Pair<String,List<SessionReport>>>> | ||||
| 
 | ||||
|     fun saveSession(newSessionReport: SessionReport) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,13 +1,52 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| 
 | ||||
| interface UserDAO { | ||||
| 
 | ||||
|     suspend fun getUsername(): String? | ||||
|     suspend fun save(newUsername: String) | ||||
|     fun getCurrentUserId(): String | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all references to this user in the database. Similar to the deleteCascade in | ||||
|      * @return all users | ||||
|      */ | ||||
|     fun getAllUsers(): Flow<List<User>> | ||||
| 
 | ||||
|     /** | ||||
|      * @return all users based on a query, a trimmed down version of getAllUsers() | ||||
|      */ | ||||
|     fun getUsersWithQuery( | ||||
|         fieldName: String, | ||||
|         value: String | ||||
|     ): Flow<List<User>> | ||||
| 
 | ||||
|     /** | ||||
|      * Request information about a user | ||||
|      */ | ||||
|     fun getUserDetails( | ||||
|         userId: String | ||||
|     ): Flow<User> | ||||
| 
 | ||||
|     suspend fun getUsername( | ||||
|         userId: String | ||||
|     ): String | ||||
| 
 | ||||
|     /** | ||||
|      * @return information on the currently logged in user. | ||||
|      */ | ||||
|     suspend fun getLoggedInUser(): User | ||||
|     // TODO Should be refactored to fun getLoggedInUser(): Flow<User>, without suspend. | ||||
| 
 | ||||
|     suspend fun saveLoggedInUser( | ||||
|         newUsername: String, | ||||
|         newBiography: String = "" | ||||
|     ) | ||||
|     // TODO Should be refactored to fun saveLoggedInUser(...): Boolean, without suspend. | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all references to the logged in user in the database. Similar to the deleteCascade in | ||||
|      * relational databases. | ||||
|      */ | ||||
|     suspend fun deleteUserReferences() | ||||
|     suspend fun deleteLoggedInUserReferences() | ||||
|     // TODO Should be refactored to fun deleteLoggedInUserReferences(): Boolean, without suspend. | ||||
| } | ||||
|  | @ -1,37 +0,0 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.SessionDAO | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FireBaseSessionDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO | ||||
| ) : SessionDAO { | ||||
| 
 | ||||
|     override fun getSessions(): Flow<List<SessionReport>> { | ||||
|         return currentUserSessionsCollection() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(SessionReport::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override fun saveSession(newSessionReport: SessionReport) { | ||||
|         currentUserSessionsCollection().add(newSessionReport) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteSession(newTimer: TimerInfo) { | ||||
|         currentUserSessionsCollection().document(newTimer.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     private fun currentUserSessionsCollection(): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SESSION_COLLECTION) | ||||
| } | ||||
|  | @ -1,8 +1,9 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| object FireBaseCollections { | ||||
| object FirebaseCollections { | ||||
|     const val SESSION_COLLECTION = "sessions" | ||||
|     const val USER_COLLECTION = "users" | ||||
|     const val FRIENDS_COLLECTION = "friends" | ||||
|     const val TIMER_COLLECTION = "timers" | ||||
|     const val SUBJECT_COLLECTION = "subjects" | ||||
|     const val TASK_COLLECTION = "tasks" | ||||
|  | @ -0,0 +1,134 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.data.local.models.Friendship | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseFriendship.ACCEPTED | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDSSINCE | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDID | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.FriendshipDAO | ||||
| import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.FRIENDS_COLLECTION | ||||
| import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION | ||||
| import com.google.firebase.Timestamp | ||||
| import com.google.firebase.firestore.DocumentReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import javax.inject.Inject | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| class FirebaseFriendshipDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO | ||||
| ): FriendshipDAO { | ||||
| 
 | ||||
|     private fun currentUserDocument(): DocumentReference = firestore | ||||
|         .collection(USER_COLLECTION) | ||||
|         .document(auth.currentUserId) | ||||
| 
 | ||||
|     override fun getAllFriendships( | ||||
|         userId: String | ||||
|     ): Flow<List<Friendship>> { | ||||
|         return firestore | ||||
|             .collection(USER_COLLECTION) | ||||
|             .document(userId) | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Friendship::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override fun getFriendshipCount( | ||||
|         userId: String | ||||
|     ): Flow<Int> { | ||||
|         return flow { | ||||
|             val friendshipCount = suspendCoroutine { continuation -> | ||||
|                 firestore | ||||
|                     .collection(USER_COLLECTION) | ||||
|                     .document(userId) | ||||
|                     .collection(FRIENDS_COLLECTION) | ||||
|                     .get() | ||||
|                     .addOnSuccessListener { querySnapshot -> | ||||
|                         continuation.resume(querySnapshot.size()) | ||||
|                     } | ||||
|                     .addOnFailureListener { exception -> | ||||
|                         continuation.resumeWithException(exception) | ||||
|                     } | ||||
|             } | ||||
|             emit(friendshipCount) | ||||
|         }.catch { | ||||
|             SnackbarManager.showMessage(AppText.generic_error) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getFriendshipDetails(id: String): Friendship { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
| 
 | ||||
|     override fun sendFriendshipRequest(id: String): Boolean { | ||||
|         val currentUserId: String = auth.currentUserId | ||||
|         val otherUserId: String = id | ||||
| 
 | ||||
|         // Add entry to current user | ||||
|         currentUserDocument() | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .add(mapOf( | ||||
|                 FRIENDID to otherUserId, | ||||
|                 ACCEPTED to true, // TODO Make it not automatically accepted. | ||||
|                 FRIENDSSINCE to Timestamp.now() | ||||
|             )) | ||||
| 
 | ||||
|         // Add entry to other user | ||||
|         firestore.collection(USER_COLLECTION) | ||||
|             .document(otherUserId) | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .add(mapOf( | ||||
|                 FRIENDID to currentUserId, | ||||
|                 ACCEPTED to true, // TODO Make it not automatically accepted. | ||||
|                 FRIENDSSINCE to Timestamp.now() | ||||
|             )) | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun acceptFriendship(id: String): Boolean { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
| 
 | ||||
|     override fun removeFriendship( | ||||
|         friendship: Friendship | ||||
|     ): Boolean { | ||||
|         val currentUserId: String = auth.currentUserId | ||||
|         val otherUserId: String = friendship.friendId | ||||
| 
 | ||||
|         // Remove at logged in user | ||||
|         firestore.collection(USER_COLLECTION) | ||||
|             .document(currentUserId) | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .document(friendship.id) | ||||
|             .delete() | ||||
| 
 | ||||
|         // Remove at other user | ||||
|         firestore.collection(USER_COLLECTION) | ||||
|             .document(otherUserId) | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .whereEqualTo(FRIENDID, currentUserId) | ||||
|             .get() | ||||
|             .addOnSuccessListener { | ||||
|                 for (document in it) { | ||||
|                     document.reference.delete() | ||||
|                 } | ||||
|             }.addOnFailureListener { | ||||
|                 SnackbarManager.showMessage(AppText.generic_error) | ||||
|             } | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,79 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseSessionReport | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.ENDTIME | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseSessionReport.STUDYTIME | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.FriendshipDAO | ||||
| import be.ugent.sel.studeez.domain.SessionDAO | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.SESSION_COLLECTION | ||||
| import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION | ||||
| import com.google.firebase.Timestamp | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.getField | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.emptyFlow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FirebaseSessionDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
|     private val userDAO: UserDAO, | ||||
|     private val friendshipDAO: FriendshipDAO | ||||
| ) : SessionDAO { | ||||
| 
 | ||||
|     override fun getSessions(): Flow<List<SessionReport>> { | ||||
|         return currentUserSessionsCollection() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(SessionReport::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSessionsOfUser(userId: String): List<SessionReport> { | ||||
|         val collection = firestore.collection(USER_COLLECTION) | ||||
|             .document(userId) | ||||
|             .collection(SESSION_COLLECTION) | ||||
|             .get().await() | ||||
|         val list: MutableList<SessionReport> = mutableListOf() | ||||
|         for (document in collection) { | ||||
|             val id = document.id | ||||
|             val studyTime: Int = document.getField<Int>(STUDYTIME)!! | ||||
|             val endTime: Timestamp = document.getField<Timestamp>(ENDTIME)!! | ||||
|             list.add(SessionReport(id, studyTime, endTime)) | ||||
|         } | ||||
|         return list | ||||
|     } | ||||
| 
 | ||||
|     override fun getFriendsSessions(): Flow<List<Pair<String, List<SessionReport>>>> { | ||||
|         return friendshipDAO.getAllFriendships(auth.currentUserId) | ||||
|             .map { friendships -> | ||||
|                 friendships.map { friendship -> | ||||
|                     val userId: String = friendship.friendId | ||||
|                     val username = userDAO.getUsername(userId) | ||||
|                     val userSessions = getSessionsOfUser(userId) | ||||
| 
 | ||||
|                     Pair(username, userSessions) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     override fun saveSession(newSessionReport: SessionReport) { | ||||
|         currentUserSessionsCollection().add(newSessionReport) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteSession(newTimer: TimerInfo) { | ||||
|         currentUserSessionsCollection().document(newTimer.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     private fun currentUserSessionsCollection(): CollectionReference = | ||||
|         firestore.collection(USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(SESSION_COLLECTION) | ||||
| } | ||||
|  | @ -19,7 +19,7 @@ import kotlinx.coroutines.tasks.await | |||
| import javax.inject.Inject | ||||
| import kotlin.collections.count | ||||
| 
 | ||||
| class FireBaseSubjectDAO @Inject constructor( | ||||
| class FirebaseSubjectDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|  | @ -50,7 +50,7 @@ class FireBaseSubjectDAO @Inject constructor( | |||
|     override suspend fun archiveSubject(subject: Subject) { | ||||
|         currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) | ||||
|         currentUserSubjectsCollection().document(subject.id) | ||||
|             .collection(FireBaseCollections.TASK_COLLECTION) | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
|             .taskNotArchived() | ||||
|             .get().await() | ||||
|             .documents | ||||
|  | @ -75,16 +75,16 @@ class FireBaseSubjectDAO @Inject constructor( | |||
|     } | ||||
| 
 | ||||
|     private fun currentUserSubjectsCollection(): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
| 
 | ||||
|     private fun subjectTasksCollection(subject: Subject): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
|             .document(subject.id) | ||||
|             .collection(FireBaseCollections.TASK_COLLECTION) | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
| 
 | ||||
|     fun CollectionReference.subjectNotArchived(): Query = | ||||
|         this.whereEqualTo(SubjectDocument.archived, false) | ||||
|  | @ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map | |||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FireBaseTaskDAO @Inject constructor( | ||||
| class FirebaseTaskDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
| ) : TaskDAO { | ||||
|  | @ -45,12 +45,11 @@ class FireBaseTaskDAO @Inject constructor( | |||
|     } | ||||
| 
 | ||||
|     private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
|             .document(subjectId) | ||||
|             .collection(FireBaseCollections.TASK_COLLECTION) | ||||
| 
 | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
| } | ||||
| 
 | ||||
| // Extend CollectionReference and Query with some filters | ||||
|  | @ -48,8 +48,8 @@ class FirebaseTimerDAO @Inject constructor( | |||
|     } | ||||
| 
 | ||||
|     private fun currentUserTimersCollection(): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.TIMER_COLLECTION) | ||||
|             .collection(FirebaseCollections.TIMER_COLLECTION) | ||||
| 
 | ||||
| } | ||||
|  | @ -2,10 +2,18 @@ package be.ugent.sel.studeez.domain.implementation | |||
| 
 | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseUser.BIOGRAPHY | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseUser.USERNAME | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.domain.implementation.FirebaseCollections.USER_COLLECTION | ||||
| import com.google.firebase.firestore.DocumentReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
|  | @ -14,22 +22,71 @@ class FirebaseUserDAO @Inject constructor( | |||
|     private val auth: AccountDAO | ||||
| ) : UserDAO { | ||||
| 
 | ||||
|     override suspend fun getUsername(): String? { | ||||
|         return currentUserDocument().get().await().getString("username") | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun save(newUsername: String) { | ||||
|         currentUserDocument().set(mapOf("username" to newUsername)) | ||||
|     override fun getCurrentUserId(): String { | ||||
|         return auth.currentUserId | ||||
|     } | ||||
| 
 | ||||
|     private fun currentUserDocument(): DocumentReference = | ||||
|         firestore.collection(USER_COLLECTION).document(auth.currentUserId) | ||||
|         firestore | ||||
|             .collection(USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val USER_COLLECTION = "users" | ||||
|     override fun getAllUsers(): Flow<List<User>> { | ||||
|         return firestore | ||||
|             .collection(USER_COLLECTION) | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(User::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteUserReferences() { | ||||
|     override fun getUsersWithQuery( | ||||
|         fieldName: String, | ||||
|         value: String | ||||
|     ): Flow<List<User>> { | ||||
|         return firestore | ||||
|             .collection(USER_COLLECTION) | ||||
|             .whereEqualTo(fieldName, value) | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(User::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override fun getUserDetails(userId: String): Flow<User> { | ||||
|         return flow { | ||||
|             val snapshot = firestore | ||||
|                 .collection(USER_COLLECTION) | ||||
|                 .document(userId) | ||||
|                 .get() | ||||
|                 .await() | ||||
|             val user = snapshot.toObject(User::class.java)!! | ||||
|             emit(user) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getUsername(userId: String): String { | ||||
|         val user = firestore.collection(USER_COLLECTION) | ||||
|             .document(userId) | ||||
|             .get().await() | ||||
|         return user.getString(USERNAME)!! | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getLoggedInUser(): User { | ||||
|         val userDocument = currentUserDocument().get().await() | ||||
|         return User( | ||||
|             username = userDocument.getString(USERNAME) ?: "", | ||||
|             biography = userDocument.getString(BIOGRAPHY) ?: "" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun saveLoggedInUser( | ||||
|         newUsername: String, | ||||
|         newBiography: String | ||||
|     ) { | ||||
|         currentUserDocument().set(mapOf( | ||||
|             USERNAME to newUsername, | ||||
|             BIOGRAPHY to newBiography | ||||
|         )) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteLoggedInUserReferences() { | ||||
|         currentUserDocument().delete() | ||||
|             .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } | ||||
|             .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } | ||||
|  |  | |||
|  | @ -31,7 +31,9 @@ object StudeezDestinations { | |||
|     const val EDIT_TASK_FORM = "edit_task" | ||||
| 
 | ||||
|     // Friends flow | ||||
|     const val FRIENDS_OVERVIEW_SCREEN = "friends_overview" | ||||
|     const val SEARCH_FRIENDS_SCREEN = "search_friends" | ||||
|     const val PUBLIC_PROFILE_SCREEN = "public_profile" | ||||
| 
 | ||||
|     // Create & edit screens | ||||
|     const val CREATE_TASK_SCREEN = "create_task" | ||||
|  |  | |||
|  | @ -14,10 +14,13 @@ import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions | |||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.screens.friends.friends_overview.FriendsOveriewRoute | ||||
| import be.ugent.sel.studeez.screens.friends.friends_search.SearchFriendsRoute | ||||
| import be.ugent.sel.studeez.screens.home.HomeRoute | ||||
| import be.ugent.sel.studeez.screens.log_in.LoginRoute | ||||
| import be.ugent.sel.studeez.screens.profile.EditProfileRoute | ||||
| import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileRoute | ||||
| import be.ugent.sel.studeez.screens.profile.ProfileRoute | ||||
| import be.ugent.sel.studeez.screens.profile.public_profile.PublicProfileRoute | ||||
| import be.ugent.sel.studeez.screens.session.SessionRoute | ||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute | ||||
| import be.ugent.sel.studeez.screens.sessions.SessionsRoute | ||||
|  | @ -70,6 +73,7 @@ fun StudeezNavGraph( | |||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|                 feedViewModel = hiltViewModel(), | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  | @ -230,8 +234,28 @@ fun StudeezNavGraph( | |||
|         } | ||||
| 
 | ||||
|         // Friends flow | ||||
|         composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) { | ||||
|             FriendsOveriewRoute( | ||||
|                 open = open, | ||||
|                 popUp = goBack, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) { | ||||
|             // TODO | ||||
|             SearchFriendsRoute( | ||||
|                 popUp = goBack, | ||||
|                 open = open, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.PUBLIC_PROFILE_SCREEN) { | ||||
|             PublicProfileRoute( | ||||
|                 popUp = goBack, | ||||
|                 open = open, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         // Create & edit screens | ||||
|  |  | |||
|  | @ -0,0 +1,288 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_overview | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.ArrowBack | ||||
| import androidx.compose.material.icons.filled.Delete | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.res.vectorResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.ProfilePicture | ||||
| import be.ugent.sel.studeez.common.composable.SearchField | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.data.local.models.Friendship | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import com.google.firebase.Timestamp | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.emptyFlow | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class FriendsOverviewActions( | ||||
|     val getFriendsFlow: () -> Flow<List<Pair<User, Friendship>>>, | ||||
|     val searchFriends: () -> Unit, | ||||
|     val onQueryStringChange: (String) -> Unit, | ||||
|     val onSubmit: () -> Unit, | ||||
|     val viewProfile: (String) -> Unit, | ||||
|     val removeFriend: (Friendship) -> Unit | ||||
| ) | ||||
| 
 | ||||
| fun getFriendsOverviewActions( | ||||
|     viewModel: FriendsOverviewViewModel, | ||||
|     open: (String) -> Unit | ||||
| ): FriendsOverviewActions { | ||||
|     return FriendsOverviewActions( | ||||
|         getFriendsFlow = viewModel::getAllFriends, | ||||
|         searchFriends = { viewModel.searchFriends(open) }, | ||||
|         onQueryStringChange = viewModel::onQueryStringChange, | ||||
|         onSubmit = { viewModel.onSubmit(open) }, | ||||
|         viewProfile = { userId -> | ||||
|             viewModel.viewProfile(userId, open) | ||||
|         }, | ||||
|         removeFriend = viewModel::removeFriend | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FriendsOveriewRoute( | ||||
|     open: (String) -> Unit, | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: FriendsOverviewViewModel | ||||
| ) { | ||||
|     val uiState by viewModel.uiState | ||||
|     FriendsOverviewScreen( | ||||
|         popUp = popUp, | ||||
|         uiState = uiState, | ||||
|         friendsOverviewActions = getFriendsOverviewActions( | ||||
|             viewModel = viewModel, | ||||
|             open = open | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FriendsOverviewScreen( | ||||
|     popUp: () -> Unit, | ||||
|     uiState: FriendsOverviewUiState, | ||||
|     friendsOverviewActions: FriendsOverviewActions | ||||
| ) { | ||||
|     val friends = friendsOverviewActions.getFriendsFlow().collectAsState(initial = emptyList()) | ||||
| 
 | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
|             TopAppBar( | ||||
|                 title = { | ||||
|                     // TODO Link to each other | ||||
|                     SearchField( | ||||
|                         value = uiState.queryString, | ||||
|                         onValueChange = friendsOverviewActions.onQueryStringChange, | ||||
|                         onSubmit = friendsOverviewActions.onSubmit, | ||||
|                         label = AppText.search_friends | ||||
|                     ) | ||||
|                 }, | ||||
|                 navigationIcon = { | ||||
|                     IconButton(onClick = popUp) { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Default.ArrowBack, | ||||
|                             contentDescription = resources().getString(R.string.go_back) | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 // TODO Add inbox action | ||||
|             ) | ||||
|         } | ||||
|     ) { paddingValues -> | ||||
|         LazyColumn ( | ||||
|             modifier = Modifier.padding(paddingValues) | ||||
|         ) { | ||||
|             if (friends.value.isEmpty()) { | ||||
|                 // Show a quick button to search friends when the user does not have any friends yet. | ||||
|                 item { | ||||
|                     BasicButton( | ||||
|                         text = AppText.no_friends, | ||||
|                         modifier = Modifier.basicButton() | ||||
|                     ) { | ||||
|                         friendsOverviewActions.searchFriends() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             items(friends.value) { friend -> | ||||
|                 FriendsEntry( | ||||
|                     user = friend.first, | ||||
|                     friendship = friend.second, | ||||
|                     viewProfile = { userId -> friendsOverviewActions.viewProfile(userId) }, | ||||
|                     removeFriend = friendsOverviewActions.removeFriend | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FriendsOverviewPreview() { | ||||
|     StudeezTheme { | ||||
|         FriendsOverviewScreen( | ||||
|             popUp = {}, | ||||
|             uiState = FriendsOverviewUiState(""), | ||||
|             friendsOverviewActions = FriendsOverviewActions( | ||||
|                 getFriendsFlow = { emptyFlow() }, | ||||
|                 searchFriends = {}, | ||||
|                 onQueryStringChange = {}, | ||||
|                 onSubmit = {}, | ||||
|                 viewProfile = {}, | ||||
|                 removeFriend = {} | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FriendsEntry( | ||||
|     user: User, | ||||
|     friendship: Friendship, | ||||
|     viewProfile: (String) -> Unit, | ||||
|     removeFriend: (Friendship) -> Unit | ||||
| ) { | ||||
|     Row ( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(horizontal = 15.dp, vertical = 7.dp), | ||||
|     ) { | ||||
|         Box( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = 4.dp) | ||||
|         ) { | ||||
|             ProfilePicture() | ||||
|         } | ||||
| 
 | ||||
|         Box ( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|         ) { | ||||
|             Column ( | ||||
|                 modifier = Modifier | ||||
|                     .padding(vertical = 4.dp) | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = user.username, | ||||
|                     fontSize = 16.sp, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis | ||||
|                 ) | ||||
|                 Text( | ||||
|                     text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}", | ||||
|                     fontSize = 14.sp, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             Box( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 contentAlignment = Alignment.CenterEnd | ||||
|             ) { | ||||
|                 FriendsOverviewDropDown( | ||||
|                     friendship = friendship, | ||||
|                     viewProfile = viewProfile, | ||||
|                     removeFriend = removeFriend | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FriendsEntryPreview() { | ||||
|     StudeezTheme { | ||||
|         FriendsEntry( | ||||
|             user = User( | ||||
|                 id = "", | ||||
|                 username = "Tibo De Peuter", | ||||
|                 biography = "short bio" | ||||
|             ), | ||||
|             friendship = Friendship( | ||||
|                 id = "", | ||||
|                 friendId = "someId", | ||||
|                 friendsSince = Timestamp.now(), | ||||
|                 accepted = true | ||||
|             ), | ||||
|             viewProfile = {}, | ||||
|             removeFriend = {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FriendsOverviewDropDown( | ||||
|     friendship: Friendship, | ||||
|     viewProfile: (String) -> Unit, | ||||
|     removeFriend: (Friendship) -> Unit | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
| 
 | ||||
|     IconButton( | ||||
|         onClick = { expanded = true } | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), | ||||
|             contentDescription = resources().getString(AppText.view_more), | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     DropdownMenu( | ||||
|         expanded = expanded, | ||||
|         onDismissRequest = { expanded = false } | ||||
|     ) { | ||||
|         DrawerEntry( | ||||
|             icon = Icons.Default.Person, | ||||
|             text = stringResource(id = AppText.show_profile) | ||||
|         ) { | ||||
|             viewProfile(friendship.friendId) | ||||
|         } | ||||
|         DrawerEntry( | ||||
|             icon = Icons.Default.Delete, | ||||
|             text = stringResource(id = AppText.remove_friend) | ||||
|         ) { | ||||
|             removeFriend(friendship) | ||||
|             expanded = false | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FriendsOverviewDropDownPreview() { | ||||
|     StudeezTheme { | ||||
|         FriendsOverviewDropDown( | ||||
|             friendship = Friendship( | ||||
|                 id = "", | ||||
|                 friendId = "someId", | ||||
|                 friendsSince = Timestamp.now(), | ||||
|                 accepted = true | ||||
|             ), | ||||
|             viewProfile = {}, | ||||
|             removeFriend = { } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,6 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_overview | ||||
| 
 | ||||
| data class FriendsOverviewUiState( | ||||
|     val userId: String, | ||||
|     val queryString: String = "" | ||||
| ) | ||||
|  | @ -0,0 +1,78 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_overview | ||||
| 
 | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import be.ugent.sel.studeez.data.SelectedUserId | ||||
| import be.ugent.sel.studeez.data.local.models.Friendship | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.domain.FriendshipDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.flatMapConcat | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class FriendsOverviewViewModel @Inject constructor( | ||||
|     private val userDAO: UserDAO, | ||||
|     private val friendshipDAO: FriendshipDAO, | ||||
|     private val selectedUserIdState: SelectedUserId, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     var uiState = mutableStateOf(FriendsOverviewUiState( | ||||
|         userId = selectedUserIdState.value | ||||
|     )) | ||||
|         private set | ||||
| 
 | ||||
|     fun getAllFriends(): Flow<List<Pair<User, Friendship>>> { | ||||
|         return friendshipDAO.getAllFriendships( | ||||
|                 userId = uiState.value.userId | ||||
|             ) | ||||
|             .flatMapConcat { friendships -> | ||||
|                 val userFlows = friendships.map { friendship -> | ||||
|                     userDAO.getUserDetails(friendship.friendId) | ||||
|                 } | ||||
|                 combine(userFlows) { users -> | ||||
|                     friendships.zip(users) { friendship, user -> | ||||
|                         Pair(user, friendship) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     fun searchFriends(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.SEARCH_FRIENDS_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun onQueryStringChange(newValue: String) { | ||||
|         uiState.value = uiState.value.copy( | ||||
|             queryString = newValue | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun onSubmit(open: (String) -> Unit) { | ||||
|         val query = uiState.value.queryString // TODO Pass as argument | ||||
|         open(StudeezDestinations.SEARCH_FRIENDS_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun viewProfile( | ||||
|         userId: String, | ||||
|         open: (String) -> Unit | ||||
|     ) { | ||||
|         selectedUserIdState.value = userId | ||||
|         open(StudeezDestinations.PUBLIC_PROFILE_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun removeFriend( | ||||
|         friendship: Friendship | ||||
|     ) { | ||||
|         friendshipDAO.removeFriendship( | ||||
|             friendship = friendship | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_search | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.emptyFlow | ||||
| 
 | ||||
| data class SearchFriendUiState( | ||||
|     val queryString: String = "", | ||||
|     val searchResults: Flow<List<User>> = emptyFlow() | ||||
| ) | ||||
|  | @ -0,0 +1,265 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_search | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.ArrowBack | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.res.vectorResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.ProfilePicture | ||||
| import be.ugent.sel.studeez.common.composable.SearchField | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class SearchFriendsActions( | ||||
|     val onQueryStringChange: (String) -> Unit, | ||||
|     val getUsersWithUsername: (String) -> Unit, | ||||
|     val getAllUsers: () -> Flow<List<User>>, | ||||
|     val goToProfile: (String) -> Unit | ||||
| ) | ||||
| 
 | ||||
| fun getSearchFriendsActions( | ||||
|     viewModel: SearchFriendsViewModel, | ||||
|     open: (String) -> Unit | ||||
| ): SearchFriendsActions { | ||||
|     return SearchFriendsActions( | ||||
|         onQueryStringChange = viewModel::onQueryStringChange, | ||||
|         getUsersWithUsername = viewModel::getUsersWithUsername, | ||||
|         getAllUsers = { viewModel.getAllUsers() }, | ||||
|         goToProfile = { userId -> viewModel.goToProfile(userId, open) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SearchFriendsRoute( | ||||
|     popUp: () -> Unit, | ||||
|     open: (String) -> Unit, | ||||
|     viewModel: SearchFriendsViewModel | ||||
| ) { | ||||
|     val uiState by viewModel.uiState | ||||
| 
 | ||||
|     SearchFriendsScreen( | ||||
|         popUp = popUp, | ||||
|         uiState = uiState, | ||||
|         searchFriendsActions = getSearchFriendsActions( | ||||
|             viewModel = viewModel, | ||||
|             open = open | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SearchFriendsScreen( | ||||
|     popUp: () -> Unit, | ||||
|     uiState: SearchFriendUiState, | ||||
|     searchFriendsActions: SearchFriendsActions | ||||
| ) { | ||||
|     var query by remember { mutableStateOf(uiState.queryString) } | ||||
|     val searchResults = searchFriendsActions.getAllUsers().collectAsState( | ||||
|         initial = emptyList() | ||||
|     ) | ||||
| 
 | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
|             TopAppBar( | ||||
|                 title = { | ||||
|                     SearchField( | ||||
|                         value = query, | ||||
|                         onValueChange = { newValue -> | ||||
|                             searchFriendsActions.onQueryStringChange(newValue) | ||||
|                             query = newValue | ||||
|                         }, | ||||
|                         onSubmit = { }, | ||||
|                         label = AppText.search_friends | ||||
|                     ) | ||||
|                 }, | ||||
|                 navigationIcon = { | ||||
|                     IconButton(onClick = popUp) { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Default.ArrowBack, | ||||
|                             contentDescription = resources().getString(R.string.go_back) | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|     ) { paddingValues -> | ||||
|         LazyColumn( | ||||
|             modifier = Modifier.padding(paddingValues) | ||||
|         ) { | ||||
|             items (searchResults.value) { user -> | ||||
|                 UserEntry( | ||||
|                     user = user, | ||||
|                     goToProfile = searchFriendsActions.goToProfile | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun SearchFriendsPreview() { | ||||
|     StudeezTheme { | ||||
|         SearchFriendsScreen( | ||||
|             popUp = {}, | ||||
|             uiState = SearchFriendUiState( | ||||
|                 queryString = "dit is een test", | ||||
|                 searchResults = flowOf(listOf(User( | ||||
|                     id = "someid", | ||||
|                     username = "Eerste user", | ||||
|                     biography = "blah blah blah" | ||||
|                 ))) | ||||
|             ), | ||||
|             searchFriendsActions = SearchFriendsActions( | ||||
|                 onQueryStringChange = {}, | ||||
|                 getUsersWithUsername = {}, | ||||
|                 getAllUsers = { | ||||
|                     flowOf(listOf(User( | ||||
|                         id = "someid", | ||||
|                         username = "Eerste user", | ||||
|                         biography = "blah blah blah" | ||||
|                     ))) | ||||
|                 }, | ||||
|                 goToProfile = { } | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun UserEntry( | ||||
|     user: User, | ||||
|     goToProfile: (String) -> Unit | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(horizontal = 15.dp, vertical = 7.dp), | ||||
|         horizontalArrangement = Arrangement.spacedBy(15.dp) | ||||
|     ) { | ||||
|         Box( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = 4.dp) | ||||
|         ) { | ||||
|             ProfilePicture() | ||||
|         } | ||||
| 
 | ||||
|         Box ( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|         ) { | ||||
|             Column ( | ||||
|                 modifier = Modifier | ||||
|                     .padding(vertical = 4.dp) | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = user.username, | ||||
|                     fontSize = 16.sp, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis | ||||
|                 ) | ||||
|                 Text( | ||||
|                     text = "${resources().getString(AppText.app_name)} ${resources().getString(AppText.friend)}", | ||||
|                     fontSize = 14.sp, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             Box( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 contentAlignment = Alignment.CenterEnd | ||||
|             ) { | ||||
|                 SearchFriendsDropDown( | ||||
|                     user = user, | ||||
|                     goToProfile = goToProfile | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun UserEntryPreview() { | ||||
|     StudeezTheme { | ||||
|         UserEntry( | ||||
|             user = User( | ||||
|                 id = "someid", | ||||
|                 username = "Eerste user", | ||||
|                 biography = "blah blah blah" | ||||
|             ), | ||||
|             goToProfile = { } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Three dots that open a dropdown menu that allow to go the users profile. | ||||
|  */ | ||||
| @Composable | ||||
| fun SearchFriendsDropDown( | ||||
|     user: User, | ||||
|     goToProfile: (String) -> Unit | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
| 
 | ||||
|     IconButton( | ||||
|         onClick = { expanded = true } | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), | ||||
|             contentDescription = stringResource(AppText.view_more), | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     DropdownMenu( | ||||
|         expanded = expanded, | ||||
|         onDismissRequest = { expanded = false } | ||||
|     ) { | ||||
|         DropdownMenuItem(onClick = { expanded = false }) { | ||||
|             DrawerEntry( | ||||
|                 icon = Icons.Default.Person, | ||||
|                 text = stringResource(id = AppText.show_profile) | ||||
|             ) { | ||||
|                 goToProfile(user.id) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun SearchFriendsDropDownPreview() { | ||||
|     StudeezTheme { | ||||
|         SearchFriendsDropDown( | ||||
|             user = User( | ||||
|                 id = "someid", | ||||
|                 username = "Eerste user", | ||||
|                 biography = "blah blah blah" | ||||
|             ), | ||||
|             goToProfile = { } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,66 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_search | ||||
| 
 | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import be.ugent.sel.studeez.data.SelectedUserId | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseUser | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.filter | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class SearchFriendsViewModel @Inject constructor( | ||||
|     private val userDAO: UserDAO, | ||||
|     private val selectedProfileState: SelectedUserId, | ||||
|     logService: LogService | ||||
| ): StudeezViewModel(logService) { | ||||
| 
 | ||||
|     var uiState = mutableStateOf(SearchFriendUiState()) | ||||
|         private set | ||||
| 
 | ||||
|     fun onQueryStringChange(newValue: String) { | ||||
|         uiState.value = uiState.value.copy( | ||||
|             queryString = newValue | ||||
|         ) | ||||
|         uiState.value = uiState.value.copy( | ||||
|             searchResults = userDAO.getUsersWithQuery( | ||||
|                 fieldName = FirebaseUser.USERNAME, | ||||
|                 value = uiState.value.queryString | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun getUsersWithUsername( | ||||
|         value: String | ||||
|     ): Flow<List<User>> { | ||||
|         return userDAO.getUsersWithQuery( | ||||
|             fieldName = FirebaseUser.USERNAME, | ||||
|             value = value | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all users, except for the current user. | ||||
|      */ | ||||
|     fun getAllUsers(): Flow<List<User>> { | ||||
|         return userDAO.getAllUsers() | ||||
|             .filter { users -> | ||||
|                 users.any { user -> | ||||
|                     user.id != userDAO.getCurrentUserId() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     fun goToProfile( | ||||
|         userId: String, | ||||
|         open: (String) -> Unit | ||||
|     ) { | ||||
|         selectedProfileState.value = userId | ||||
|         open(StudeezDestinations.PUBLIC_PROFILE_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -21,14 +21,15 @@ import be.ugent.sel.studeez.resources | |||
| @Composable | ||||
| fun HomeRoute( | ||||
|     open: (String) -> Unit, | ||||
|     viewModel: HomeViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedViewModel: FeedViewModel, | ||||
| ) { | ||||
|     val feedUiState by feedViewModel.uiState.collectAsState() | ||||
|     HomeScreen( | ||||
|         onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, | ||||
|         drawerActions = drawerActions, | ||||
|         open = open, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         feedUiState = feedUiState, | ||||
|         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, | ||||
|  | @ -38,7 +39,7 @@ fun HomeRoute( | |||
| 
 | ||||
| @Composable | ||||
| fun HomeScreen( | ||||
|     open: (String) -> Unit, | ||||
|     onViewFriendsClick: () -> Unit, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedUiState: FeedUiState, | ||||
|  | @ -49,15 +50,17 @@ fun HomeScreen( | |||
|         title = resources().getString(R.string.home), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         // TODO barAction = { FriendsAction() } | ||||
|         barAction = { FriendsAction(onViewFriendsClick) } | ||||
|     ) { | ||||
|         Feed(feedUiState, continueTask, onEmptyFeedHelp) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FriendsAction() { | ||||
|     IconButton(onClick = { /*TODO*/ }) { | ||||
| fun FriendsAction( | ||||
|     onClick: () -> Unit | ||||
| ) { | ||||
|     IconButton(onClick = onClick) { | ||||
|         Icon( | ||||
|             imageVector = Icons.Default.Person, | ||||
|             contentDescription = resources().getString(R.string.friends) | ||||
|  | @ -69,9 +72,9 @@ fun FriendsAction() { | |||
| @Composable | ||||
| fun HomeScreenPreview() { | ||||
|     HomeScreen( | ||||
|         onViewFriendsClick = {}, | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||
|         open = {}, | ||||
|         feedUiState = FeedUiState.Succes( | ||||
|             mapOf( | ||||
|                 "08 May 2023" to listOf( | ||||
|  |  | |||
|  | @ -0,0 +1,17 @@ | |||
| package be.ugent.sel.studeez.screens.home | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class HomeViewModel @Inject constructor( | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
| 
 | ||||
|     fun onViewFriendsClick(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -1,5 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| 
 | ||||
| data class ProfileEditUiState ( | ||||
|     val username: String = "" | ||||
| ) | ||||
|  | @ -1,37 +1,50 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.IconButton | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Edit | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.Headline | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.ext.defaultButtonShape | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.emptyFlow | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class ProfileActions( | ||||
|     val getUsername: suspend CoroutineScope.() -> String?, | ||||
|     val getBiography: suspend CoroutineScope.() -> String?, | ||||
|     val getAmountOfFriends: () -> Flow<Int>, | ||||
|     val onEditProfileClick: () -> Unit, | ||||
|     val onViewFriendsClick: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| fun getProfileActions( | ||||
|     viewModel: ProfileViewModel, | ||||
|     open: (String) -> Unit, | ||||
|     open: (String) -> Unit | ||||
| ): ProfileActions { | ||||
|     return ProfileActions( | ||||
|         getUsername = { viewModel.getUsername() }, | ||||
|         getBiography = { viewModel.getBiography() }, | ||||
|         getAmountOfFriends = { viewModel.getAmountOfFriends() }, | ||||
|         onEditProfileClick = { viewModel.onEditProfileClick(open) }, | ||||
|         onViewFriendsClick = { viewModel.onViewFriendsClick(open) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -56,8 +69,12 @@ fun ProfileScreen( | |||
|     navigationBarActions: NavigationBarActions, | ||||
| ) { | ||||
|     var username: String? by remember { mutableStateOf("") } | ||||
|     var biography: String? by remember { mutableStateOf("") } | ||||
|     val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0) | ||||
| 
 | ||||
|     LaunchedEffect(key1 = Unit) { | ||||
|         username = profileActions.getUsername(this) | ||||
|         biography = profileActions.getBiography(this) | ||||
|     } | ||||
|     PrimaryScreenTemplate( | ||||
|         title = resources().getString(AppText.profile), | ||||
|  | @ -65,7 +82,35 @@ fun ProfileScreen( | |||
|         navigationBarActions = navigationBarActions, | ||||
|         barAction = { EditAction(onClick = profileActions.onEditProfileClick) } | ||||
|     ) { | ||||
|         Headline(text = (username ?: resources().getString(R.string.no_username))) | ||||
|         LazyColumn( | ||||
|             verticalArrangement = Arrangement.spacedBy(15.dp) | ||||
|         ) { | ||||
|             item { | ||||
|                 Headline(text = username ?: resources().getString(AppText.no_username)) | ||||
|             } | ||||
| 
 | ||||
|             item { | ||||
|                 Row( | ||||
|                     horizontalArrangement = Arrangement.spacedBy(5.dp), | ||||
|                     modifier = Modifier.fillMaxWidth() | ||||
|                         .wrapContentWidth(align = Alignment.CenterHorizontally) | ||||
|                 ) { | ||||
|                     AmountOfFriendsButton( | ||||
|                         amountOfFriends = amountOfFriends.value | ||||
|                     ) { | ||||
|                         profileActions.onViewFriendsClick() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             item { | ||||
|                 Text( | ||||
|                     text = biography ?: "", | ||||
|                     textAlign = TextAlign.Center, | ||||
|                     modifier = Modifier.padding(48.dp, 0.dp) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -78,7 +123,6 @@ fun EditAction( | |||
|             imageVector = Icons.Default.Edit, | ||||
|             contentDescription = resources().getString(AppText.edit_profile) | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -86,8 +130,38 @@ fun EditAction( | |||
| @Composable | ||||
| fun ProfileScreenPreview() { | ||||
|     ProfileScreen( | ||||
|         profileActions = ProfileActions({ null }, {}), | ||||
|         profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}), | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun AmountOfFriendsButton( | ||||
|     amountOfFriends: Int, | ||||
|     onClick: () -> Unit | ||||
| ){ | ||||
|     Button( | ||||
|         onClick = onClick, | ||||
|         shape = defaultButtonShape() | ||||
|     ) { | ||||
|         Text( | ||||
|             text = resources().getQuantityString( | ||||
|                 /* id = */ R.plurals.friends_amount, | ||||
|                 /* quantity = */ amountOfFriends, | ||||
|                 /* ...formatArgs = */ amountOfFriends | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun AmountOfFriendsButtonPreview() { | ||||
|     StudeezTheme { | ||||
|         Column { | ||||
|             AmountOfFriendsButton(amountOfFriends = 1) { } | ||||
|             AmountOfFriendsButton(amountOfFriends = 100) { } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,24 +1,40 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| 
 | ||||
| import be.ugent.sel.studeez.domain.FriendshipDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import com.google.firebase.auth.FirebaseAuth | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class ProfileViewModel @Inject constructor( | ||||
|     private val userDAO: UserDAO, | ||||
|     private val friendshipDAO: FriendshipDAO, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     suspend fun getUsername(): String? { | ||||
|         return userDAO.getUsername() | ||||
|     suspend fun getUsername(): String { | ||||
|         return userDAO.getLoggedInUser().username | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getBiography(): String { | ||||
|         return userDAO.getLoggedInUser().biography | ||||
|     } | ||||
| 
 | ||||
|     fun getAmountOfFriends(): Flow<Int> { | ||||
|         return friendshipDAO.getFriendshipCount(userDAO.getCurrentUserId()) | ||||
|     } | ||||
| 
 | ||||
|     fun onEditProfileClick(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.EDIT_PROFILE_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun onViewFriendsClick(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,20 +1,21 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| package be.ugent.sel.studeez.screens.profile.edit_profile | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicTextButton | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.ext.textButton | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class EditProfileActions( | ||||
|     val onUserNameChange: (String) -> Unit, | ||||
|     val onBiographyChange: (String) -> Unit, | ||||
|     val onSaveClick: () -> Unit, | ||||
|     val onDeleteClick: () -> Unit | ||||
| ) | ||||
|  | @ -25,6 +26,7 @@ fun getEditProfileActions( | |||
| ): EditProfileActions { | ||||
|     return EditProfileActions( | ||||
|         onUserNameChange = { viewModel.onUsernameChange(it) }, | ||||
|         onBiographyChange = { viewModel.onBiographyChange(it) }, | ||||
|         onSaveClick = { viewModel.onSaveClick() }, | ||||
|         onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, | ||||
|     ) | ||||
|  | @ -51,36 +53,49 @@ fun EditProfileScreen( | |||
|     editProfileActions: EditProfileActions, | ||||
| ) { | ||||
|     SecondaryScreenTemplate( | ||||
|         title = resources().getString(R.string.editing_profile), | ||||
|         title = resources().getString(AppText.editing_profile), | ||||
|         popUp = goBack | ||||
|     ) { | ||||
|         Column { | ||||
|         LazyColumn { | ||||
|             item { | ||||
|                 LabelledInputField( | ||||
|                     value = uiState.username, | ||||
|                     onNewValue = editProfileActions.onUserNameChange, | ||||
|                 label = R.string.username | ||||
|                     label = AppText.username | ||||
|                 ) | ||||
|             } | ||||
|             item { | ||||
|                 LabelledInputField( | ||||
|                     value = uiState.biography, | ||||
|                     onNewValue = editProfileActions.onBiographyChange, | ||||
|                     label = AppText.biography | ||||
|                 ) | ||||
|             } | ||||
|             item { | ||||
|                 BasicTextButton( | ||||
|                 text = R.string.save, | ||||
|                     text = AppText.save, | ||||
|                     Modifier.textButton(), | ||||
|                     action = { | ||||
|                         editProfileActions.onSaveClick() | ||||
|                         goBack() | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|             item { | ||||
|                  BasicTextButton( | ||||
|                 text = R.string.delete_profile, | ||||
|                     text = AppText.delete_profile, | ||||
|                     Modifier.textButton(), | ||||
|                     action = editProfileActions.onDeleteClick | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun EditProfileScreenComposable() { | ||||
|     StudeezTheme { | ||||
|         EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {})) | ||||
|         EditProfileScreen({}, ProfileEditUiState(), EditProfileActions({}, {}, {}, {})) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,6 @@ | |||
| package be.ugent.sel.studeez.screens.profile.edit_profile | ||||
| 
 | ||||
| data class ProfileEditUiState ( | ||||
|     val username: String = "", | ||||
|     val biography: String = "" | ||||
| ) | ||||
|  | @ -1,8 +1,9 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| package be.ugent.sel.studeez.screens.profile.edit_profile | ||||
| 
 | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
|  | @ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor( | |||
| 
 | ||||
|     init { | ||||
|         launchCatching { | ||||
|             uiState.value = uiState.value.copy(username = userDAO.getUsername()!!) | ||||
|             val user: User = userDAO.getLoggedInUser() | ||||
|             uiState.value = uiState.value.copy( | ||||
|                 username = user.username, | ||||
|                 biography = user.biography | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -31,16 +36,23 @@ class ProfileEditViewModel @Inject constructor( | |||
|         uiState.value = uiState.value.copy(username = newValue) | ||||
|     } | ||||
| 
 | ||||
|     fun onBiographyChange(newValue: String) { | ||||
|         uiState.value = uiState.value.copy(biography = newValue) | ||||
|     } | ||||
| 
 | ||||
|     fun onSaveClick() { | ||||
|         launchCatching { | ||||
|             userDAO.save(uiState.value.username) | ||||
|             userDAO.saveLoggedInUser( | ||||
|                 newUsername = uiState.value.username, | ||||
|                 newBiography = uiState.value.biography | ||||
|             ) | ||||
|             SnackbarManager.showMessage(R.string.success) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { | ||||
|         launchCatching { | ||||
|             userDAO.deleteUserReferences() // Delete references | ||||
|             userDAO.deleteLoggedInUserReferences() // Delete references | ||||
|             accountDAO.deleteAccount() // Delete authentication | ||||
|         } | ||||
|         openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) | ||||
|  | @ -0,0 +1,178 @@ | |||
| package be.ugent.sel.studeez.screens.profile.public_profile | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.MailOutline | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.res.vectorResource | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.Headline | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerEntry | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.profile.AmountOfFriendsButton | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class PublicProfileActions( | ||||
|     val getUserDetails: () -> Flow<User>, | ||||
|     val getAmountOfFriends: () -> Flow<Int>, | ||||
|     val onViewFriendsClick: () -> Unit, | ||||
|     val sendFriendRequest: () -> Boolean | ||||
| ) | ||||
| 
 | ||||
| fun getPublicProfileActions( | ||||
|     viewModel: PublicProfileViewModel, | ||||
|     open: (String) -> Unit | ||||
| ): PublicProfileActions { | ||||
|     return PublicProfileActions( | ||||
|         getUserDetails = { viewModel.getUserDetails(viewModel.uiState.value.userId) }, | ||||
|         getAmountOfFriends = { viewModel.getAmountOfFriends( | ||||
|             userId = viewModel.uiState.value.userId | ||||
|         ) }, | ||||
|         onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, | ||||
|         sendFriendRequest = { viewModel.sendFriendRequest( | ||||
|             userId = viewModel.uiState.value.userId | ||||
|         ) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun PublicProfileRoute( | ||||
|     popUp: () -> Unit, | ||||
|     open: (String) -> Unit, | ||||
|     viewModel: PublicProfileViewModel | ||||
| ) { | ||||
|     PublicProfileScreen( | ||||
|         publicProfileActions = getPublicProfileActions( | ||||
|             viewModel = viewModel, | ||||
|             open = open | ||||
|         ), | ||||
|         popUp = popUp | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun PublicProfileScreen( | ||||
|     publicProfileActions: PublicProfileActions, | ||||
|     popUp: () -> Unit | ||||
| ) { | ||||
|     val user = publicProfileActions.getUserDetails().collectAsState(initial = User()) | ||||
|     val amountOfFriends = publicProfileActions.getAmountOfFriends().collectAsState(initial = 0) | ||||
| 
 | ||||
|     SecondaryScreenTemplate( | ||||
|         title = stringResource(id = AppText.profile), | ||||
|         popUp = popUp, | ||||
|         barAction = { | ||||
|             PublicProfileEllipsis( | ||||
|                 sendFriendRequest = publicProfileActions.sendFriendRequest | ||||
|             ) | ||||
|         } | ||||
|     ) { | ||||
|         LazyColumn( | ||||
|             verticalArrangement = Arrangement.spacedBy(15.dp) | ||||
|         ) { | ||||
|             item { | ||||
|                 Headline(text = user.value.username) | ||||
|             } | ||||
| 
 | ||||
|             item { | ||||
|                 Row( | ||||
|                     horizontalArrangement = Arrangement.spacedBy(5.dp), | ||||
|                     modifier = Modifier | ||||
|                         .fillMaxWidth() | ||||
|                         .wrapContentWidth(align = Alignment.CenterHorizontally) | ||||
|                 ) { | ||||
|                     AmountOfFriendsButton( | ||||
|                         amountOfFriends = amountOfFriends.value | ||||
|                     ) { | ||||
|                         publicProfileActions.onViewFriendsClick() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             item { | ||||
|                 Text( | ||||
|                     text = user.value.biography, | ||||
|                     textAlign = TextAlign.Center, | ||||
|                     modifier = Modifier.padding(48.dp, 0.dp) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun PublicProfilePreview() { | ||||
|     StudeezTheme { | ||||
|         PublicProfileScreen( | ||||
|             publicProfileActions = PublicProfileActions( | ||||
|                 getUserDetails = { | ||||
|                     flowOf(User( | ||||
|                         id = "someid", | ||||
|                         username = "Maxime De Poorter", | ||||
|                         biography = "I am a different student and this is my public profile" | ||||
|                     )) | ||||
|                 }, | ||||
|                 getAmountOfFriends = { flowOf(113) }, | ||||
|                 onViewFriendsClick = {}, | ||||
|                 sendFriendRequest = { true } | ||||
|             ), | ||||
|             popUp = {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun PublicProfileEllipsis( | ||||
|     sendFriendRequest: () -> Boolean | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
| 
 | ||||
|     IconButton( | ||||
|         onClick = { expanded = true } | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = ImageVector.vectorResource(id = R.drawable.ic_more_horizontal), | ||||
|             contentDescription = resources().getString(AppText.view_more), | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     DropdownMenu( | ||||
|         expanded = expanded, | ||||
|         onDismissRequest = { expanded = false } | ||||
|     ) { | ||||
|         DropdownMenuItem(onClick = { expanded = false }) { | ||||
|             DrawerEntry( | ||||
|                 icon = Icons.Default.MailOutline, | ||||
|                 text = stringResource(id = AppText.send_friend_request) | ||||
|             ) { | ||||
|                 sendFriendRequest() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun PublicProfileEllipsisPreview() { | ||||
|     StudeezTheme { | ||||
|         PublicProfileEllipsis( | ||||
|             sendFriendRequest = { true } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| package be.ugent.sel.studeez.screens.profile.public_profile | ||||
| 
 | ||||
| data class PublicProfileUiState( | ||||
|     var userId: String = "" | ||||
| ) | ||||
|  | @ -0,0 +1,60 @@ | |||
| package be.ugent.sel.studeez.screens.profile.public_profile | ||||
| 
 | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import be.ugent.sel.studeez.data.SelectedUserId | ||||
| import be.ugent.sel.studeez.data.local.models.User | ||||
| import be.ugent.sel.studeez.domain.FriendshipDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.UserDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class PublicProfileViewModel @Inject constructor( | ||||
|     private val userDAO: UserDAO, | ||||
|     private val friendshipDAO: FriendshipDAO, | ||||
|     selectedUserIdState: SelectedUserId, | ||||
|     logService: LogService | ||||
| ): StudeezViewModel(logService) { | ||||
| 
 | ||||
|     val uiState = mutableStateOf( | ||||
|         PublicProfileUiState( | ||||
|             userId = selectedUserIdState.value | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|     fun getUserDetails( | ||||
|         userId: String | ||||
|     ): Flow<User> { | ||||
|         uiState.value = uiState.value.copy( | ||||
|             userId = userId | ||||
|         ) | ||||
|         return userDAO.getUserDetails( | ||||
|             userId = uiState.value.userId | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun getAmountOfFriends( | ||||
|         userId: String | ||||
|     ): Flow<Int> { | ||||
|         return friendshipDAO.getFriendshipCount( | ||||
|             userId = userId | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun onViewFriendsClick( | ||||
|         open: (String) -> Unit | ||||
|     ) { | ||||
|         open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun sendFriendRequest( | ||||
|         userId: String | ||||
|     ): Boolean { | ||||
|         return friendshipDAO.sendFriendshipRequest(userId) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -10,9 +10,9 @@ object InvisibleSessionManager { | |||
|     private var viewModel: SessionViewModel? = null | ||||
|     private lateinit var mediaPlayer: MediaPlayer | ||||
| 
 | ||||
|     fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { | ||||
|     fun setParameters(viewModel: SessionViewModel, mediaPlayer: MediaPlayer) { | ||||
|         this.mediaPlayer = mediaPlayer | ||||
|         this.viewModel = viewModel | ||||
|         this.mediaPlayer = mediaplayer | ||||
|     } | ||||
| 
 | ||||
|     suspend fun updateTimer() { | ||||
|  |  | |||
|  | @ -6,28 +6,22 @@ import android.net.Uri | |||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreenComposable | ||||
| 
 | ||||
| data class SessionActions( | ||||
|     val getTimer: () -> FunctionalTimer, | ||||
|     val getTask: () -> String, | ||||
|     val startMediaPlayer: () -> Unit, | ||||
|     val releaseMediaPlayer: () -> Unit, | ||||
|     val endSession: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| private fun getSessionActions( | ||||
|     viewModel: SessionViewModel, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     mediaplayer: MediaPlayer, | ||||
| ): SessionActions { | ||||
|     return SessionActions( | ||||
|         getTimer = viewModel::getTimer, | ||||
|         getTask = viewModel::getTask, | ||||
|         endSession = { viewModel.endSession(openAndPopUp) }, | ||||
|         startMediaPlayer = mediaplayer::start, | ||||
|         releaseMediaPlayer = mediaplayer::release, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -37,20 +31,15 @@ fun SessionRoute( | |||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: SessionViewModel, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|     val mediaplayer = MediaPlayer.create(context, uri) | ||||
|     mediaplayer.isLooping = false | ||||
|     val mediaPlayer = MediaPlayer.create(LocalContext.current, uri) | ||||
|     mediaPlayer.isLooping = false | ||||
| 
 | ||||
|     InvisibleSessionManager.setParameters( | ||||
|         viewModel = viewModel, | ||||
|         mediaplayer = mediaplayer | ||||
|     ) | ||||
|     InvisibleSessionManager.setParameters(viewModel = viewModel, mediaPlayer = mediaPlayer) | ||||
| 
 | ||||
|     val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen(mediaplayer)) | ||||
|     val soundPlayer = SoundPlayer(LocalContext.current) | ||||
|     val sessionActions = getSessionActions(viewModel, openAndPopUp) | ||||
|     val sessionScreen = viewModel.getTimer().accept(GetSessionScreenComposable(soundPlayer, open, sessionActions)) | ||||
| 
 | ||||
|     sessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer) | ||||
|     ) | ||||
|     sessionScreen() | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,29 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.media.MediaPlayer | ||||
| import android.media.RingtoneManager | ||||
| 
 | ||||
| class SoundPlayer(private val context: Context) { | ||||
| 
 | ||||
|     var oldValue: Boolean = false | ||||
|     var mediaPlayer: MediaPlayer = initPlayer() | ||||
| 
 | ||||
|     fun playOn(newValue: Boolean) { | ||||
|         if (oldValue != newValue) { | ||||
|             mediaPlayer.start() | ||||
|             mediaPlayer.setOnCompletionListener { | ||||
|                 mediaPlayer = initPlayer() | ||||
|             } | ||||
|             oldValue = newValue | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private fun initPlayer(): MediaPlayer { | ||||
|         return  MediaPlayer.create( | ||||
|             context, | ||||
|             RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -1,150 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| abstract class AbstractSessionScreen { | ||||
| 
 | ||||
|     @Composable | ||||
|     operator fun invoke( | ||||
|         open: (String) -> Unit, | ||||
|         sessionActions: SessionActions, | ||||
|     ) { | ||||
|         Column( | ||||
|             modifier = Modifier.padding(10.dp) | ||||
|         ) { | ||||
|             Timer( | ||||
|                 sessionActions = sessionActions, | ||||
|             ) | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp) | ||||
|             ) { | ||||
|                 TextButton( | ||||
|                     onClick = { | ||||
|                         sessionActions.releaseMediaPlayer | ||||
|                         sessionActions.endSession() | ||||
|                     }, | ||||
|                     modifier = Modifier | ||||
|                         .padding(horizontal = 20.dp) | ||||
|                         .border(1.dp, Color.Red, RoundedCornerShape(32.dp)) | ||||
|                         .background(Color.Transparent) | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = "End session", | ||||
|                         color = Color.Red, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         fontSize = 18.sp, | ||||
|                         modifier = Modifier.padding(1.dp) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun Timer( | ||||
|         sessionActions: SessionActions, | ||||
|     ) { | ||||
|         var tikker by remember { mutableStateOf(false) } | ||||
|         LaunchedEffect(tikker) { | ||||
|             delay(1.seconds) | ||||
|             sessionActions.getTimer().tick() | ||||
|             callMediaPlayer() | ||||
|             tikker = !tikker | ||||
|         } | ||||
| 
 | ||||
|         val hms = sessionActions.getTimer().getHoursMinutesSeconds() | ||||
|         Column { | ||||
|             Text( | ||||
|                 text = hms.toString(), | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp), | ||||
|                 textAlign = TextAlign.Center, | ||||
|                 fontWeight = FontWeight.Bold, | ||||
|                 fontSize = 40.sp, | ||||
|             ) | ||||
| 
 | ||||
|             Text( | ||||
|                 text = motivationString(), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 textAlign = TextAlign.Center, | ||||
|                 fontWeight = FontWeight.Light, | ||||
|                 fontSize = 30.sp | ||||
|             ) | ||||
| 
 | ||||
|             MidSection() | ||||
| 
 | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp) | ||||
|             ) { | ||||
|                 Box( | ||||
|                     contentAlignment = Alignment.Center, | ||||
|                     modifier = Modifier | ||||
|                         .padding(16.dp) | ||||
|                         .background(Color.Blue, RoundedCornerShape(32.dp)) | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = sessionActions.getTask(), | ||||
|                         color = Color.White, | ||||
|                         fontSize = 18.sp, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     abstract fun motivationString(): String | ||||
| 
 | ||||
|     @Composable | ||||
|     open fun MidSection() { | ||||
|         // Default has no midsection, unless overwritten. | ||||
|     } | ||||
| 
 | ||||
|     abstract fun callMediaPlayer() | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun TimerPreview() { | ||||
|     val sessionScreen = object : AbstractSessionScreen() { | ||||
|         @Composable | ||||
|         override fun motivationString(): String = "Test" | ||||
|         override fun callMediaPlayer() {} | ||||
| 
 | ||||
|     } | ||||
|     sessionScreen.Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}, {})) | ||||
| } | ||||
|  | @ -1,96 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| class BreakSessionScreen( | ||||
|     private val funPomoDoroTimer: FunctionalPomodoroTimer, | ||||
|     private var mediaplayer: MediaPlayer? | ||||
| ) : AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun MidSection() { | ||||
|         Dots() | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun Dots() { | ||||
|         Row( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|             horizontalArrangement = Arrangement.Center, | ||||
|         ) { | ||||
|             if (funPomoDoroTimer.hasEnded()) { | ||||
|                 repeat(funPomoDoroTimer.repeats) { | ||||
|                     Dot(Color.Green) | ||||
|                 } | ||||
|             } else { | ||||
|                 repeat(funPomoDoroTimer.repeats - funPomoDoroTimer.breaksRemaining - 1) { | ||||
|                     Dot(color = Color.DarkGray) | ||||
|                 } | ||||
|                 if (!funPomoDoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray) | ||||
|                 repeat(funPomoDoroTimer.breaksRemaining) { | ||||
|                     Dot(color = Color.Gray) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     private fun Dot(color: Color) { | ||||
|         Box( | ||||
|             modifier = Modifier | ||||
|                 .padding(5.dp) | ||||
|                 .size(10.dp) | ||||
|                 .clip(CircleShape) | ||||
|                 .background(color) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         if (funPomoDoroTimer.isInBreak) { | ||||
|             return resources().getString(AppText.state_take_a_break) | ||||
|         } | ||||
| 
 | ||||
|         if (funPomoDoroTimer.hasEnded()) { | ||||
|             return resources().getString(AppText.state_done) | ||||
|         } | ||||
| 
 | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() { | ||||
|         if (funPomoDoroTimer.hasEnded()) { | ||||
|             mediaplayer?.let { it: MediaPlayer -> | ||||
|                 it.setOnCompletionListener { | ||||
|                     it.release() | ||||
|                     mediaplayer = null | ||||
|                 } | ||||
|                 it.start() | ||||
|             } | ||||
|         } else if (funPomoDoroTimer.hasCurrentCountdownEnded()) { | ||||
|             mediaplayer?.start() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun MidsectionPreview() { | ||||
|     val funPomoDoroTimer = FunctionalPomodoroTimer(15, 60, 5) | ||||
|     val breakSessionScreen = BreakSessionScreen(funPomoDoroTimer, MediaPlayer()) | ||||
|     breakSessionScreen.MidSection() | ||||
| } | ||||
|  | @ -0,0 +1,79 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| @Composable | ||||
| fun BreakSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     pomodoroTimer: FunctionalPomodoroTimer, | ||||
|     soundPlayer: SoundPlayer, | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = sessionActions, | ||||
|         midSection = { Dots(pomodoroTimer = pomodoroTimer) }, | ||||
|         callMediaPlayer = { soundPlayer.playOn(pomodoroTimer.hasCurrentCountdownEnded()) }, | ||||
|         motivationString = { motivationString (pomodoroTimer = pomodoroTimer) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun Dots(pomodoroTimer: FunctionalPomodoroTimer): Int { | ||||
|     Row( | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|         horizontalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         if (pomodoroTimer.hasEnded()) { | ||||
|             repeat(pomodoroTimer.repeats) { | ||||
|                 Dot(Color.Green) | ||||
|             } | ||||
|         } else { | ||||
|             repeat(pomodoroTimer.repeats - pomodoroTimer.breaksRemaining - 1) { | ||||
|                 Dot(color = Color.DarkGray) | ||||
|             } | ||||
|             if (!pomodoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray) | ||||
|             repeat(pomodoroTimer.breaksRemaining) { | ||||
|                 Dot(color = Color.Gray) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return pomodoroTimer.breaksRemaining | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun Dot(color: Color) { | ||||
|     Box(modifier = Modifier | ||||
|         .padding(5.dp) | ||||
|         .size(10.dp) | ||||
|         .clip(CircleShape) | ||||
|         .background(color)) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(pomodoroTimer: FunctionalPomodoroTimer): String { | ||||
|     if (pomodoroTimer.isInBreak) { | ||||
|         return resources().getString(R.string.state_take_a_break) | ||||
|     } | ||||
| 
 | ||||
|     if (pomodoroTimer.hasEnded()) { | ||||
|         return resources().getString(R.string.state_done) | ||||
|     } | ||||
| 
 | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,35 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| 
 | ||||
| class CustomSessionScreen( | ||||
|     private val functionalTimer: FunctionalCustomTimer, | ||||
|     private var mediaplayer: MediaPlayer? | ||||
| ): AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         if (functionalTimer.hasEnded()) { | ||||
|             return resources().getString(AppText.state_done) | ||||
|         } | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() { | ||||
|         if (functionalTimer.hasEnded()) { | ||||
|             mediaplayer?.let { it: MediaPlayer -> | ||||
|                 it.setOnCompletionListener { | ||||
|                     it.release() | ||||
|                     mediaplayer = null | ||||
|                 } | ||||
|                 it.start() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,32 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| @Composable | ||||
| fun CustomTimerSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     customTimer: FunctionalCustomTimer, | ||||
|     soundPlayer: SoundPlayer | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         callMediaPlayer = { soundPlayer.playOn(customTimer.hasEnded()) }, | ||||
|         sessionActions = sessionActions | ||||
|     ) { | ||||
|         motivationString(customTimer = customTimer) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(customTimer: FunctionalCustomTimer): String { | ||||
|     if (customTimer.hasEnded()) { | ||||
|         return resources().getString(R.string.state_done) | ||||
|     } | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,16 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| 
 | ||||
| class EndlessSessionScreen : AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() {} | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| 
 | ||||
| @Composable | ||||
| fun EndlessTimerSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = sessionActions | ||||
|     ) { | ||||
|         motivationString() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(): String { | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,18 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor | ||||
| 
 | ||||
| class GetSessionScreen(private val mediaplayer: MediaPlayer?) : FunctionalTimerVisitor<AbstractSessionScreen> { | ||||
|     override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen = | ||||
|         CustomSessionScreen(functionalCustomTimer, mediaplayer) | ||||
| 
 | ||||
|     override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen = | ||||
|         EndlessSessionScreen() | ||||
| 
 | ||||
|     override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen = | ||||
|         BreakSessionScreen(functionalPomodoroTimer, mediaplayer) | ||||
| } | ||||
|  | @ -0,0 +1,47 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| class GetSessionScreenComposable( | ||||
|     private val soundPlayer: SoundPlayer, | ||||
|     private val open: (String) -> Unit, | ||||
|     private val sessionActions: SessionActions | ||||
|     ) : | ||||
|     FunctionalTimerVisitor<@Composable () -> Unit> { | ||||
| 
 | ||||
|     override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): @Composable () -> Unit { | ||||
|         return { CustomTimerSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|                 soundPlayer = soundPlayer, | ||||
|                 customTimer = functionalCustomTimer, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): @Composable () -> Unit { | ||||
|         return { | ||||
|             EndlessTimerSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): @Composable () -> Unit { | ||||
|         return { | ||||
|             BreakSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|                 soundPlayer = soundPlayer, | ||||
|                 pomodoroTimer = functionalPomodoroTimer | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,73 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionScreen( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     callMediaPlayer: () -> Unit = {}, | ||||
|     midSection: @Composable () -> Int = {0}, | ||||
|     motivationString: @Composable () -> String, | ||||
| 
 | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier.padding(10.dp) | ||||
|     ) { | ||||
|         Timer( | ||||
|             sessionActions = sessionActions, | ||||
|             callMediaPlayer = callMediaPlayer, | ||||
|             motivationString = motivationString, | ||||
|             MidSection = midSection | ||||
|         ) | ||||
|         Box( | ||||
|             contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(50.dp) | ||||
|         ) { | ||||
|             EndSessionButton(sessionActions = sessionActions) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EndSessionButton(sessionActions: SessionActions) { | ||||
|     TextButton( | ||||
|         onClick = { | ||||
|             sessionActions.endSession() | ||||
|         }, | ||||
|         modifier = Modifier | ||||
|             .padding(horizontal = 20.dp) | ||||
|             .border(1.dp, Color.Red, RoundedCornerShape(32.dp)) | ||||
|             .background(Color.Transparent) | ||||
|     ) { | ||||
|         EndsessionText() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EndsessionText() { | ||||
|     Text( | ||||
|         text = "End session", | ||||
|         color = Color.Red, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         fontSize = 18.sp, | ||||
|         modifier = Modifier.padding(1.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,95 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| @Composable | ||||
| fun Timer( | ||||
|     sessionActions: SessionActions, | ||||
|     callMediaPlayer: () -> Unit, | ||||
|     motivationString: @Composable () -> String, | ||||
|     MidSection: @Composable () -> Int | ||||
| ) { | ||||
|     var tikker by remember { mutableStateOf(false) } | ||||
|     LaunchedEffect(tikker) { | ||||
|         delay(1.seconds) | ||||
|         sessionActions.getTimer().tick() | ||||
|         callMediaPlayer() | ||||
|         tikker = !tikker | ||||
|     } | ||||
| 
 | ||||
|     val hms = sessionActions.getTimer().getHoursMinutesSeconds() | ||||
|     Column { | ||||
| 
 | ||||
|         TimerClock(hms) | ||||
|         MotivationText(text = motivationString()) | ||||
|         MidSection() | ||||
| 
 | ||||
|         Box( | ||||
|             contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(50.dp) | ||||
|         ) { | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, | ||||
|                 modifier = Modifier | ||||
|                     .padding(16.dp) | ||||
|                     .background(Color.Blue, RoundedCornerShape(32.dp)) | ||||
|             ) { | ||||
|                 TaskText(taskName = sessionActions.getTask()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerClock(hms: HoursMinutesSeconds) { | ||||
|     Text( | ||||
|         text = hms.toString(), | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(50.dp), | ||||
|         textAlign = TextAlign.Center, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         fontSize = 40.sp, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun MotivationText(text: String) { | ||||
|     Text( | ||||
|         text = text, | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         textAlign = TextAlign.Center, | ||||
|         fontWeight = FontWeight.Light, | ||||
|         fontSize = 30.sp | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TaskText(taskName: String) { | ||||
|     Text( | ||||
|         text = taskName, | ||||
|         color = Color.White, | ||||
|         fontSize = 18.sp, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -66,7 +66,7 @@ class SignUpViewModel @Inject constructor( | |||
|         launchCatching { | ||||
|             accountDAO.signUpWithEmailAndPassword(email, password) | ||||
|             accountDAO.signInWithEmailAndPassword(email, password) | ||||
|             userDAO.save(username) | ||||
|             userDAO.saveLoggedInUser(username) | ||||
|             openAndPopUp(HOME_SCREEN, SIGN_UP_SCREEN) | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ import be.ugent.sel.studeez.common.composable.BasicButton | |||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.FormComposable | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | ||||
| import be.ugent.sel.studeez.common.ext.generateRandomArgb | ||||
|  |  | |||
|  | @ -2,8 +2,6 @@ package be.ugent.sel.studeez.screens.subjects.form | |||
| 
 | ||||
| import androidx.compose.runtime.MutableState | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import be.ugent.sel.studeez.common.ext.generateRandomArgb | ||||
| import be.ugent.sel.studeez.data.SelectedSubject | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ package be.ugent.sel.studeez.screens.timer_form | |||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.FormComposable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
|  | @ -12,8 +13,16 @@ fun TimerAddRoute( | |||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
|     TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.add_timer) { | ||||
|         viewModel.saveTimer(it, goBack = popUp) | ||||
| 
 | ||||
| 
 | ||||
|     TimerFormScreen( | ||||
|         popUp = popUp, | ||||
|         getTimerInfo = viewModel::getTimerInfo, | ||||
|         extraButton= { }, | ||||
|         AppText.add_timer | ||||
|     ) { | ||||
|         viewModel.saveTimer(it, goBack = {popUp(); popUp()}) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -22,7 +31,20 @@ fun TimerEditRoute( | |||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
|     TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.edit_timer) { | ||||
| 
 | ||||
|     @Composable | ||||
|     fun deleteButton() { | ||||
|         DeleteButton(text = AppText.delete_timer) { | ||||
|             viewModel.deleteTimer(viewModel.getTimerInfo(), popUp) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     TimerFormScreen( | ||||
|         popUp = popUp, | ||||
|         getTimerInfo = viewModel::getTimerInfo, | ||||
|         extraButton= { deleteButton() }, | ||||
|         AppText.edit_timer | ||||
|     ) { | ||||
|         viewModel.editTimer(it, goBack = popUp) | ||||
|     } | ||||
| } | ||||
|  | @ -31,6 +53,7 @@ fun TimerEditRoute( | |||
| fun TimerFormScreen( | ||||
|     popUp: () -> Unit, | ||||
|     getTimerInfo: () -> TimerInfo, | ||||
|     extraButton: @Composable () -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     onConfirmClick: (TimerInfo) -> Unit | ||||
| ) { | ||||
|  | @ -40,6 +63,6 @@ fun TimerFormScreen( | |||
|         title = stringResource(id = label), | ||||
|         popUp = popUp | ||||
|     ) { | ||||
|         timerFormScreen(onConfirmClick) | ||||
|         timerFormScreen(onConfirmClick, extraButton) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -23,6 +23,11 @@ class TimerFormViewModel @Inject constructor( | |||
|         goBack() | ||||
|     } | ||||
| 
 | ||||
|     fun deleteTimer(timerInfo: TimerInfo, goBack: () -> Unit) { | ||||
|         timerDAO.deleteTimer(timerInfo) | ||||
|         goBack() | ||||
|     } | ||||
| 
 | ||||
|     fun saveTimer(timerInfo: TimerInfo, goBack: () -> Unit) { | ||||
|         timerDAO.saveTimer(timerInfo) | ||||
|         goBack() | ||||
|  |  | |||
|  | @ -3,47 +3,80 @@ package be.ugent.sel.studeez.screens.timer_form.form_screens | |||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.input.KeyboardType | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
| import be.ugent.sel.studeez.common.composable.LabeledErrorTextField | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| abstract class AbstractTimerFormScreen(private val timerInfo: TimerInfo) { | ||||
| 
 | ||||
|     protected val valids = mutableMapOf( | ||||
|         "name" to mutableStateOf(textPredicate(timerInfo.name)), | ||||
|         "description" to mutableStateOf(textPredicate(timerInfo.description)) | ||||
|     ) | ||||
| 
 | ||||
|     protected val firsts = mutableMapOf( | ||||
|         "name" to mutableStateOf(true), | ||||
|         "description" to mutableStateOf(true) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|     @Composable | ||||
|     operator fun invoke(onSaveClick: (TimerInfo) -> Unit) { | ||||
| 
 | ||||
|         var name by remember { mutableStateOf(timerInfo.name) } | ||||
|         var description by remember { mutableStateOf(timerInfo.description) } | ||||
| 
 | ||||
|         // This shall rerun whenever name and description change | ||||
|         timerInfo.name = name | ||||
|         timerInfo.description = description | ||||
|     operator fun invoke( | ||||
|         onSaveClick: (TimerInfo) -> Unit, | ||||
|         extraButton: @Composable () -> Unit = {}, | ||||
|     ) { | ||||
| 
 | ||||
|         Column { | ||||
| 
 | ||||
|             // Fields that every timer shares (ommited id) | ||||
|             LabelledInputField( | ||||
|                 value = name, | ||||
|                 onNewValue = { name = it }, | ||||
|                 label = R.string.name | ||||
|             ) | ||||
|             LabeledErrorTextField( | ||||
|                 initialValue = timerInfo.name, | ||||
|                 label = R.string.name, | ||||
|                 errorText = AppText.name_error, | ||||
|                 isValid = valids.getValue("name"), | ||||
|                 isFirst = firsts.getValue("name"), | ||||
|                 keyboardType = KeyboardType.Text, | ||||
|                 predicate = { it.isNotBlank() } | ||||
|             ) { correctName -> | ||||
|                 timerInfo.name = correctName | ||||
|             } | ||||
| 
 | ||||
|             LabelledInputField( | ||||
|                 value = description, | ||||
|                 onNewValue = { description = it }, | ||||
|                 label = AppText.description, | ||||
|                 singleLine = false | ||||
|             ) | ||||
|             LabeledErrorTextField( | ||||
|                 initialValue = timerInfo.description, | ||||
|                 label = R.string.description, | ||||
|                 errorText = AppText.description_error, | ||||
|                 isValid = valids.getValue("description"), | ||||
|                 isFirst = firsts.getValue("description"), | ||||
|                 singleLine = false, | ||||
|                 keyboardType = KeyboardType.Text, | ||||
|                 predicate = { textPredicate(it) } | ||||
|             ) { correctName -> | ||||
|                 timerInfo.description = correctName | ||||
|             } | ||||
| 
 | ||||
|             ExtraFields() | ||||
| 
 | ||||
|             BasicButton(R.string.save, Modifier.basicButton()) { | ||||
|                 if (valids.all { it.component2().value }) { // All fields are valid | ||||
|                     onSaveClick(timerInfo) | ||||
|                 } else { | ||||
|                     firsts.map { | ||||
|                         it.component2().value = false | ||||
|                     } // dont mask error because its not been filled out yet | ||||
|                     SnackbarManager.showMessage(AppText.fill_out_error) | ||||
|                 } | ||||
|             } | ||||
|             extraButton() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun textPredicate(text: String): Boolean { | ||||
|         return text.isNotBlank() | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|  |  | |||
|  | @ -15,6 +15,8 @@ class BreakTimerFormScreen( | |||
|     private val breakTimerInfo: PomodoroTimerInfo | ||||
| ): AbstractTimerFormScreen(breakTimerInfo) { | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun ExtraFields() { | ||||
|         // If the user presses the OK button on the timepicker, the time in the button should change | ||||
|  | @ -26,12 +28,17 @@ class BreakTimerFormScreen( | |||
|             breakTimerInfo.breakTime = newTime | ||||
|         } | ||||
| 
 | ||||
|         valids["repeats"] = remember {mutableStateOf(true)} | ||||
|         firsts["repeats"] = remember { mutableStateOf(true) } | ||||
| 
 | ||||
|         LabeledErrorTextField( | ||||
|             initialValue = breakTimerInfo.repeats.toString(), | ||||
|             label = R.string.repeats, | ||||
|             errorText = AppText.repeats_error, | ||||
|             isValid = valids.getValue("repeats"), | ||||
|             isFirst = firsts.getValue("repeats"), | ||||
|             keyboardType = KeyboardType.Decimal, | ||||
|             predicate = { it.matches(Regex("[1-9]+\\d*")) } | ||||
|             predicate = { isNumber(it) } | ||||
|         ) { correctlyTypedInt -> | ||||
|             breakTimerInfo.repeats = correctlyTypedInt.toInt() | ||||
|         } | ||||
|  | @ -39,6 +46,10 @@ class BreakTimerFormScreen( | |||
|     } | ||||
| } | ||||
| 
 | ||||
| fun isNumber(text: String): Boolean { | ||||
|     return text.matches(Regex("[1-9]+\\d*")) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun BreakEditScreenPreview() { | ||||
|  |  | |||
|  | @ -1,13 +1,14 @@ | |||
| package be.ugent.sel.studeez.screens.timer_form.timer_type_select | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| 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 be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.* | ||||
|  | @ -37,10 +38,22 @@ fun TimerTypeSelectScreen( | |||
|         ) { | ||||
|             TimerType.values().forEach { timerType -> | ||||
|                 val default: TimerInfo = defaultTimerInfo.getValue(timerType) | ||||
|                 Button(onClick = { viewModel.onTimerTypeChosen(default, open) }) { | ||||
|                 Button( | ||||
|                     onClick = { viewModel.onTimerTypeChosen(default, open) }, | ||||
|                     modifier = Modifier.fillMaxWidth().padding(5.dp) | ||||
|                 ) { | ||||
|                     Text(text = timerType.name) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun TimerTypeSelectScreenPreview() { | ||||
|     TimerTypeSelectScreen( | ||||
|         open = {}, | ||||
|         popUp = {} | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_more_horizontal.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_more_horizontal.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| <vector android:height="24dp" android:tint="#000000" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> | ||||
| </vector> | ||||
|  | @ -16,11 +16,12 @@ | |||
|     <string name="go_back">Go back</string> | ||||
|     <string name="next">Next</string> | ||||
|     <string name="start">Start</string> | ||||
|     <string name="view_more">View more</string> | ||||
| 
 | ||||
|     <!-- Messages --> | ||||
|     <string name="success">Success!</string> | ||||
|     <string name="try_again">Try again</string> | ||||
|     <string name="generic_error">Something wrong happened. Please try again.</string> | ||||
|     <string name="generic_error">Something went wrong. Please try again.</string> | ||||
|     <string name="email_error">Please insert a valid email.</string> | ||||
| 
 | ||||
|     <!-- ========== NavBar ========== --> | ||||
|  | @ -63,6 +64,7 @@ | |||
|     <string name="edit_profile">Edit profile</string> | ||||
|     <string name="editing_profile">Editing profile</string> | ||||
|     <string name="delete_profile">Delete profile</string> | ||||
|     <string name="biography">Bio</string> | ||||
| 
 | ||||
|     <!-- ========== Drawer ========== --> | ||||
| 
 | ||||
|  | @ -72,8 +74,15 @@ | |||
| 
 | ||||
|     <!-- Timers --> | ||||
|     <string name="timers">Timers</string> | ||||
|     <string name="delete_timer">Delete Timer</string> | ||||
|     <string name="edit">Edit</string> | ||||
|     <string name="add_timer">Add timer</string> | ||||
| 
 | ||||
|     <string name="name_error">Name should not be blank</string> | ||||
|     <string name="description_error">Description should not be blank</string> | ||||
|     <string name="fill_out_error">Fill out all the fields correctly!</string> | ||||
| 
 | ||||
| 
 | ||||
|     <string name="pick_time">Select time</string> | ||||
|     <string name="state_focus">Focus!</string> | ||||
|     <plurals name="state_focus_remaining"> | ||||
|  | @ -115,7 +124,16 @@ | |||
| 
 | ||||
|     <string name="friends">Friends</string> | ||||
|     <string name="friend">Friend</string> | ||||
|     <plurals name="friends_amount"> | ||||
|         <item quantity="one">%d Friend</item> | ||||
|         <item quantity="other">%d Friends</item> | ||||
|     </plurals> | ||||
|     <string name="add_friend_not_possible_yet">Adding friends still needs to be implemented. Hang on tight!</string> <!-- TODO Remove this description line once implemented. --> | ||||
|     <string name="no_friends">You don\'t have any friends yet. Add one!</string> | ||||
|     <string name="search_friends">Search friends</string> | ||||
|     <string name="send_friend_request">Send friend request</string> | ||||
|     <string name="remove_friend">Remove as friend</string> | ||||
|     <string name="show_profile">Show profile</string> | ||||
| 
 | ||||
|     <!-- ========== Create & edit screens ========== --> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,13 +7,14 @@ import org.junit.Test | |||
| class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | ||||
|     private val breakTime = 10 | ||||
|     private val breaks = 2 | ||||
|     private val repeats = 3 // = breaks + 1 | ||||
|     override val hours = 0 | ||||
|     override val minutes = 0 | ||||
|     override val seconds = 10 | ||||
|     private lateinit var pomodoroTimer: FunctionalPomodoroTimer | ||||
| 
 | ||||
|     override fun setTimer() { | ||||
|         pomodoroTimer = FunctionalPomodoroTimer(time, breakTime, breaks) | ||||
|         pomodoroTimer = FunctionalPomodoroTimer(time, breakTime, repeats) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import be.ugent.sel.studeez.data.SelectedSessionReport | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.data.SelectedTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.implementation.LogServiceImpl | ||||
| import be.ugent.sel.studeez.screens.session.InvisibleSessionManager | ||||
| import be.ugent.sel.studeez.screens.session.SessionViewModel | ||||
|  | @ -22,13 +21,13 @@ import org.mockito.kotlin.mock | |||
| class InvisibleSessionManagerTest { | ||||
|     private var selectedTimer: SelectedTimer = SelectedTimer() | ||||
|     private lateinit var viewModel: SessionViewModel | ||||
|     private var mediaPlayer: MediaPlayer = mock() | ||||
| 
 | ||||
| 
 | ||||
|     @Test | ||||
|     fun InvisibleEndlessTimerTest() = runTest { | ||||
|         selectedTimer.set(FunctionalEndlessTimer()) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  | @ -47,10 +46,10 @@ class InvisibleSessionManagerTest { | |||
|     fun InvisiblePomodoroTimerTest() = runTest { | ||||
|         val studyTime = 10 | ||||
|         val breakTime = 5 | ||||
|         val repeats = 1 | ||||
|         val repeats = 2 | ||||
|         selectedTimer.set(FunctionalPomodoroTimer(studyTime, breakTime, repeats)) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  | @ -82,8 +81,8 @@ class InvisibleSessionManagerTest { | |||
|     @Test | ||||
|     fun InvisibleCustomTimerTest() = runTest { | ||||
|         selectedTimer.set(FunctionalCustomTimer(5)) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  |  | |||
|  | @ -21,4 +21,3 @@ plugins { | |||
|     // Hilt | ||||
|     id 'com.google.dagger.hilt.android' version '2.44' apply false | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,3 +23,5 @@ kotlin.code.style=official | |||
| # resources declared in the library itself and none from the library's dependencies, | ||||
| # thereby reducing the size of the R class for that library | ||||
| android.nonTransitiveRClass=true | ||||
| android.defaults.buildfeatures.buildconfig=true | ||||
| android.nonFinalResIds=false | ||||
		Reference in a new issue
	
	 brreynie
						brreynie