Merge branch 'development' into refactor
This commit is contained in:
		
						commit
						6542d2dbf2
					
				
					 141 changed files with 5058 additions and 1136 deletions
				
			
		
							
								
								
									
										3
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
								
							|  | @ -1,7 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <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"> | ||||
|  |  | |||
|  | @ -123,9 +123,6 @@ dependencies { | |||
|     implementation 'com.google.firebase:firebase-firestore-ktx' | ||||
|     implementation 'com.google.firebase:firebase-perf-ktx' | ||||
|     implementation 'com.google.firebase:firebase-config-ktx' | ||||
| 
 | ||||
|     // Colorpicker | ||||
|     implementation 'com.github.skydoves:colorpicker-compose:1.0.2' | ||||
| } | ||||
| 
 | ||||
| // Allow references to generate code | ||||
|  |  | |||
							
								
								
									
										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) | ||||
|     } | ||||
| } | ||||
|  | @ -2,19 +2,9 @@ package be.ugent.sel.studeez.common.composable | |||
| 
 | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.BorderStroke | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.ButtonColors | ||||
| import androidx.compose.material.ButtonDefaults | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Add | ||||
| import androidx.compose.runtime.Composable | ||||
|  | @ -31,7 +21,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 | ||||
|  | @ -48,6 +42,7 @@ fun BasicButton( | |||
|     modifier: Modifier = Modifier, | ||||
|     colors: ButtonColors = ButtonDefaults.buttonColors(), | ||||
|     border: BorderStroke? = null, | ||||
|     enabled: Boolean = true, | ||||
|     onClick: () -> Unit, | ||||
| ) { | ||||
|     Button( | ||||
|  | @ -56,6 +51,7 @@ fun BasicButton( | |||
|         shape = defaultButtonShape(), | ||||
|         colors = colors, | ||||
|         border = border, | ||||
|         enabled = enabled, | ||||
|     ) { | ||||
|         Text( | ||||
|             text = stringResource(text), | ||||
|  | @ -74,17 +70,22 @@ fun BasicButtonPreview() { | |||
| fun StealthButton( | ||||
|     @StringRes text: Int, | ||||
|     modifier: Modifier = Modifier.card(), | ||||
|     enabled: Boolean = true, | ||||
|     onClick: () -> Unit, | ||||
| ) { | ||||
|     //val clickablemodifier = if (disabled) Modifier.clickable(indication = null) else modifier | ||||
|     val borderColor = if (enabled) MaterialTheme.colors.primary | ||||
|                       else MaterialTheme.colors.onSurface.copy(alpha = 0.3f) | ||||
|     BasicButton( | ||||
|         text = text, | ||||
|         onClick = onClick, | ||||
|         modifier = modifier, | ||||
|         enabled = enabled, | ||||
|         colors = ButtonDefaults.buttonColors( | ||||
|             backgroundColor = MaterialTheme.colors.surface, | ||||
|             contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.4f) | ||||
|             contentColor = borderColor | ||||
|         ), | ||||
|         border = BorderStroke(3.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.4f)) | ||||
|         border = BorderStroke(2.dp, borderColor) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ package be.ugent.sel.studeez.common.composable | |||
| 
 | ||||
| import androidx.compose.animation.core.animateFloat | ||||
| import androidx.compose.animation.core.updateTransition | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.FloatingActionButton | ||||
| import androidx.compose.material.Icon | ||||
|  |  | |||
|  | @ -0,0 +1,22 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| 
 | ||||
| @Composable | ||||
| fun FormComposable( | ||||
|     title: String, | ||||
|     popUp: () -> Unit, | ||||
|     content: @Composable () -> Unit, | ||||
| ) { | ||||
|     SecondaryScreenTemplate(title = title, popUp = popUp) { | ||||
|         Box( | ||||
|             modifier = Modifier.verticalScroll(rememberScrollState()), | ||||
|         ) { | ||||
|             content() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,39 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.painter.Painter | ||||
| import androidx.compose.ui.unit.dp | ||||
| 
 | ||||
| @Composable | ||||
| fun ImageBackgroundButton( | ||||
|     paint: Painter, | ||||
|     str: String, | ||||
|     background2: Color, | ||||
|     setBackground1: (Color) -> Unit, | ||||
|     setBackground2: (Color) -> Unit | ||||
| ) { | ||||
|     Image( | ||||
|         painter = paint, | ||||
|         str, | ||||
|         modifier = Modifier | ||||
|             .clickable { | ||||
|                 if (background2 == Color.Transparent) { | ||||
|                     setBackground1(Color.LightGray) | ||||
|                     setBackground2(Color.Transparent) | ||||
|                 } else { | ||||
|                     setBackground2(Color.Transparent) | ||||
|                 } | ||||
|             } | ||||
|             .border( | ||||
|                 width = 2.dp, | ||||
|                 color = background2, | ||||
|                 shape = RoundedCornerShape(16.dp) | ||||
|             ) | ||||
|     ) | ||||
| } | ||||
|  | @ -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() | ||||
|     } | ||||
| } | ||||
|  | @ -3,10 +3,13 @@ package be.ugent.sel.studeez.common.composable | |||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| 
 | ||||
| @Composable | ||||
|  | @ -24,3 +27,13 @@ fun Headline( | |||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun DateText(date: String) { | ||||
|     Text( | ||||
|         text = date, | ||||
|         fontWeight = FontWeight.Medium, | ||||
|         fontSize = 20.sp, | ||||
|         modifier = Modifier.padding(horizontal = 10.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -3,13 +3,13 @@ package be.ugent.sel.studeez.common.composable | |||
| import androidx.annotation.StringRes | ||||
| 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,7 +22,6 @@ 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 kotlin.math.sin | ||||
| import be.ugent.sel.studeez.R.drawable as AppIcon | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
|  | @ -47,7 +46,7 @@ fun LabelledInputField( | |||
|     value: String, | ||||
|     onNewValue: (String) -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     singleLine: Boolean = false | ||||
|     singleLine: Boolean = true | ||||
| ) { | ||||
|     OutlinedTextField( | ||||
|         value = value, | ||||
|  | @ -119,7 +118,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 +129,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 +217,35 @@ private fun PasswordField( | |||
|         visualTransformation = visualTransformation | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SearchField( | ||||
|     value: String, | ||||
|     onValueChange: (String) -> Unit, | ||||
|     onSubmit: () -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     modifier: Modifier = Modifier, | ||||
|     enabled: Boolean = true | ||||
| ) { | ||||
|     OutlinedTextField( | ||||
|         value = value, | ||||
|         onValueChange = onValueChange, | ||||
|         modifier = modifier, | ||||
|         enabled = enabled, | ||||
|         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 | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,160 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.MaterialTheme | ||||
| 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.text.font.FontWeight | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.common.composable.BasicTextButton | ||||
| import be.ugent.sel.studeez.common.composable.DateText | ||||
| import be.ugent.sel.studeez.common.composable.Headline | ||||
| import be.ugent.sel.studeez.common.ext.textButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun Feed( | ||||
|     uiState: FeedUiState, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit | ||||
| ) { | ||||
|     when (uiState) { | ||||
|         FeedUiState.Loading -> LoadingFeed() | ||||
|         is FeedUiState.Succes -> LoadedFeed( | ||||
|             uiState = uiState, | ||||
|             continueTask = continueTask, | ||||
|             onEmptyFeedHelp = onEmptyFeedHelp | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun LoadedFeed( | ||||
|     uiState: FeedUiState.Succes, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit, | ||||
| ) { | ||||
|     if (uiState.feedEntries.isEmpty()) EmptyFeed(onEmptyFeedHelp) | ||||
|     else FeedWithElements(uiState = uiState, continueTask = continueTask) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun LoadingFeed() { | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .fillMaxHeight(), | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|         horizontalAlignment = Alignment.CenterHorizontally | ||||
|     ) { | ||||
|         CircularProgressIndicator(color = MaterialTheme.colors.onBackground) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FeedWithElements( | ||||
|     uiState: FeedUiState.Succes, | ||||
|     continueTask: (String, String) -> Unit, | ||||
| ) { | ||||
|     val feedEntries = uiState.feedEntries | ||||
|     LazyColumn { | ||||
|         items(feedEntries.toList()) { (date, feedEntries) -> | ||||
|             Row( | ||||
|                 horizontalArrangement = Arrangement.SpaceBetween, | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(10.dp), | ||||
|                 verticalAlignment = Alignment.CenterVertically | ||||
|             ) { | ||||
|                 val totalDayStudyTime: Int = feedEntries.sumOf { it.totalStudyTime } | ||||
|                 DateText(date = date) | ||||
|                 Text( | ||||
|                     text = "${HoursMinutesSeconds(totalDayStudyTime)}", | ||||
|                     fontSize = 15.sp, | ||||
|                     fontWeight = FontWeight.Medium | ||||
|                 ) | ||||
|             } | ||||
|             feedEntries.forEach { feedEntry -> | ||||
|                 FeedEntry(feedEntry = feedEntry) { | ||||
|                     continueTask(feedEntry.subjectId, feedEntry.taskId) | ||||
|                 } | ||||
|             } | ||||
|             Spacer(modifier = Modifier.height(20.dp)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EmptyFeed(onEmptyFeedHelp: () -> Unit) { | ||||
|     Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { | ||||
|         Column( | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|             modifier = Modifier.fillMaxWidth() | ||||
|         ) { | ||||
|             Headline(text = stringResource(id = AppText.your_feed)) | ||||
| 
 | ||||
|             BasicTextButton( | ||||
|                 AppText.empty_feed_help_text, | ||||
|                 Modifier.textButton(), | ||||
|                 action = onEmptyFeedHelp, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedLoadingPreview() { | ||||
|     Feed( | ||||
|         uiState = FeedUiState.Loading, | ||||
|         continueTask = { _, _ -> run {} }, {} | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedPreview() { | ||||
|     Feed( | ||||
|         uiState = FeedUiState.Succes( | ||||
|             mapOf( | ||||
|                 "08 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 600, | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 20, | ||||
|                     ), | ||||
|                 ), | ||||
|                 "09 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFD1200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                 ) | ||||
|             ) | ||||
|         ), | ||||
|         continueTask = { _, _ -> run {} }, {} | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,116 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.Card | ||||
| import androidx.compose.material.Text | ||||
| 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.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun FeedEntry( | ||||
|     feedEntry: FeedEntry, | ||||
|     continueWithTask: () -> Unit, | ||||
| ) { | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(horizontal = 10.dp, vertical = 5.dp), | ||||
|     ) { | ||||
|         Row( | ||||
|             horizontalArrangement = Arrangement.SpaceBetween, | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             Row( | ||||
|                 horizontalArrangement = Arrangement.spacedBy(8.dp), | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = 10.dp) | ||||
|                     .weight(11f) | ||||
|             ) { | ||||
|                 Box( | ||||
|                     modifier = Modifier | ||||
|                         .size(20.dp) | ||||
|                         .clip(CircleShape) | ||||
|                         .background(Color(feedEntry.argb_color)), | ||||
|                 ) | ||||
|                 Row( | ||||
|                     modifier = Modifier.fillMaxWidth(), | ||||
|                     horizontalArrangement = Arrangement.SpaceBetween, | ||||
|                     verticalAlignment = Alignment.CenterVertically, | ||||
|                 ) { | ||||
|                     Column( | ||||
|                         verticalArrangement = Arrangement.spacedBy(0.dp) | ||||
|                     ) { | ||||
|                         Text( | ||||
|                             text = feedEntry.subJectName, | ||||
|                             fontWeight = FontWeight.Medium, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             maxLines = 1, | ||||
|                         ) | ||||
|                         Text( | ||||
|                             text = feedEntry.taskName, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             maxLines = 1, | ||||
|                         ) | ||||
|                     } | ||||
|                     Text(text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString()) | ||||
|                 } | ||||
|             } | ||||
|             val buttonText: Int = | ||||
|                 if (feedEntry.isArchived) AppText.deleted else AppText.continue_task | ||||
|             StealthButton( | ||||
|                 text = buttonText, | ||||
|                 enabled = !feedEntry.isArchived, | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = 10.dp, end = 5.dp) | ||||
|                     .weight(6f) | ||||
|             ) { | ||||
|                 if (!feedEntry.isArchived) { | ||||
|                     continueWithTask() | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedEntryPreview() { | ||||
|     FeedEntry( | ||||
|         continueWithTask = {}, | ||||
|         feedEntry = FeedEntry( | ||||
|             argb_color = 0xFFFFD200, | ||||
|             subJectName = "Test Subject", | ||||
|             taskName = "Test Task", | ||||
|             totalStudyTime = 20, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedEntryOverflowPreview() { | ||||
|     FeedEntry( | ||||
|         continueWithTask = {}, | ||||
|         feedEntry = FeedEntry( | ||||
|             argb_color = 0xFFFFD200, | ||||
|             subJectName = "Test Subject", | ||||
|             taskName = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkk", | ||||
|             totalStudyTime = 20, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| 
 | ||||
| sealed interface FeedUiState { | ||||
|     object Loading : FeedUiState | ||||
|     data class Succes(val feedEntries: Map<String, List<FeedEntry>>) : FeedUiState | ||||
| } | ||||
|  | @ -0,0 +1,45 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.domain.FeedDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.SharingStarted | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.stateIn | ||||
| import kotlinx.coroutines.launch | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class FeedViewModel @Inject constructor( | ||||
|     feedDAO: FeedDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|     private val selectedTask: SelectedTask, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     val uiState: StateFlow<FeedUiState> = feedDAO.getFeedEntries() | ||||
|         .map { FeedUiState.Succes(it) } | ||||
|         .stateIn( | ||||
|             scope = viewModelScope, | ||||
|             initialValue = FeedUiState.Loading, | ||||
|             started = SharingStarted.Eagerly, | ||||
|         ) | ||||
| 
 | ||||
|     fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) { | ||||
|         viewModelScope.launch { | ||||
|             val task = taskDAO.getTask(subjectId, taskId) | ||||
|             selectedTask.set(task) | ||||
|             open(StudeezDestinations.TIMER_SELECTION_SCREEN) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun onEmptyFeedHelp(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.ADD_SUBJECT_FORM) | ||||
|     } | ||||
| } | ||||
|  | @ -8,13 +8,15 @@ import androidx.compose.material.icons.Icons | |||
| import androidx.compose.material.icons.filled.Check | ||||
| import androidx.compose.material.icons.filled.List | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.material.icons.outlined.Check | ||||
| import androidx.compose.material.icons.outlined.DateRange | ||||
| import androidx.compose.material.icons.outlined.Face | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
|  | @ -99,11 +101,11 @@ fun NavigationBar( | |||
|         BottomNavigationItem( | ||||
|             icon = { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.DateRange, resources().getString(AppText.sessions) | ||||
|                     imageVector = Icons.Outlined.Face, resources().getString(AppText.friends_feed) | ||||
|                 ) | ||||
|             }, | ||||
|             label = { Text(text = resources().getString(AppText.sessions)) }, | ||||
|             selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN), | ||||
|             label = { Text(text = resources().getString(AppText.friends_feed)) }, | ||||
|             selected = navigationBarActions.isSelectedTab(FRIENDS_FEED), | ||||
|             onClick = navigationBarActions.onSessionsClick | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,9 +2,11 @@ package be.ugent.sel.studeez.common.composable.navbar | |||
| 
 | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.FRIENDS_FEED | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SESSIONS_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SEARCH_FRIENDS_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SELECT_SUBJECT | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.SUBJECT_SCREEN | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
|  | @ -25,7 +27,7 @@ class NavigationBarViewModel @Inject constructor( | |||
|     } | ||||
| 
 | ||||
|     fun onSessionsClick(open: (String) -> Unit) { | ||||
|         open(SESSIONS_SCREEN) | ||||
|         open(FRIENDS_FEED) | ||||
|     } | ||||
| 
 | ||||
|     fun onProfileClick(open: (String) -> Unit) { | ||||
|  | @ -33,13 +35,11 @@ class NavigationBarViewModel @Inject constructor( | |||
|     } | ||||
| 
 | ||||
|     fun onAddTaskClick(open: (String) -> Unit) { | ||||
|         // TODO open(CREATE_TASK_SCREEN) | ||||
|         SnackbarManager.showMessage(AppText.create_task_not_possible_yet) // TODO Remove | ||||
|         open(SELECT_SUBJECT) | ||||
|     } | ||||
| 
 | ||||
|     fun onAddFriendClick(open: (String) -> Unit) { | ||||
|         // TODO open(SEARCH_FRIENDS_SCREEN) | ||||
|         SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove | ||||
|         open(SEARCH_FRIENDS_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun onAddSessionClick(open: (String) -> Unit) { | ||||
|  |  | |||
|  | @ -1,20 +1,17 @@ | |||
| package be.ugent.sel.studeez.common.composable.tasks | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.Card | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.List | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
|  | @ -24,16 +21,24 @@ import androidx.compose.ui.text.font.FontWeight | |||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun SubjectEntry( | ||||
|     subject: Subject, | ||||
|     onViewSubject: () -> Unit, | ||||
|     getTaskCount: () -> Flow<Int>, | ||||
|     getCompletedTaskCount: () -> Flow<Int>, | ||||
|     getStudyTime: () -> Flow<Int>, | ||||
|     selectButton: @Composable (RowScope) -> Unit, | ||||
| ) { | ||||
|     val studytime by getStudyTime().collectAsState(initial = 0) | ||||
|     val taskCount by getTaskCount().collectAsState(initial = 0) | ||||
|     val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0) | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|  | @ -61,16 +66,17 @@ fun SubjectEntry( | |||
|                 ) { | ||||
|                     Text( | ||||
|                         text = subject.name, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         overflow = TextOverflow.Ellipsis, | ||||
|                         maxLines = 1, | ||||
|                         fontWeight = FontWeight.Medium | ||||
|                     ) | ||||
|                     Row( | ||||
|                         horizontalArrangement = Arrangement.spacedBy(10.dp), | ||||
|                         verticalAlignment = Alignment.CenterVertically, | ||||
|                         verticalAlignment = Alignment.CenterVertically | ||||
|                     ) { | ||||
|                         Text( | ||||
|                             text = HoursMinutesSeconds(subject.time).toString(), | ||||
|                             text = HoursMinutesSeconds(studytime).toString(), | ||||
|                             color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f) | ||||
|                         ) | ||||
|                         Row( | ||||
|                             verticalAlignment = Alignment.CenterVertically, | ||||
|  | @ -78,21 +84,18 @@ fun SubjectEntry( | |||
|                         ) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Default.List, | ||||
|                                 contentDescription = stringResource(id = AppText.tasks) | ||||
|                                 contentDescription = stringResource(id = AppText.tasks), | ||||
|                                 tint = MaterialTheme.colors.onBackground.copy(alpha = 0.6f) | ||||
|                             ) | ||||
|                             Text( | ||||
|                                 text = "${completedTaskCount}/${taskCount}", | ||||
|                                 color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f) | ||||
|                             ) | ||||
|                             Text(text = "0/0") // TODO | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             StealthButton( | ||||
|                 text = AppText.view_tasks, | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = 10.dp, end = 5.dp) | ||||
|                     .weight(1f) | ||||
|             ) { | ||||
|                 onViewSubject() | ||||
|             } | ||||
|             selectButton(this) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -104,10 +107,18 @@ fun SubjectEntryPreview() { | |||
|         subject = Subject( | ||||
|             name = "Test Subject", | ||||
|             argb_color = 0xFFFFD200, | ||||
|             time = 60 | ||||
|         ), | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|     ) { | ||||
|         StealthButton( | ||||
|             text = AppText.view_tasks, | ||||
|             modifier = Modifier | ||||
|                 .padding(start = 10.dp, end = 5.dp) | ||||
|         ) {} | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
|  | @ -116,7 +127,9 @@ fun OverflowSubjectEntryPreview() { | |||
|         subject = Subject( | ||||
|             name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt", | ||||
|             argb_color = 0xFFFFD200, | ||||
|             time = 60 | ||||
|         ), | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|     ) {} | ||||
| } | ||||
|  | @ -1,17 +1,7 @@ | |||
| package be.ugent.sel.studeez.common.composable.tasks | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.Card | ||||
| import androidx.compose.material.Checkbox | ||||
| import androidx.compose.material.CheckboxDefaults | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.IconButton | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Delete | ||||
| import androidx.compose.runtime.Composable | ||||
|  | @ -31,7 +21,8 @@ import be.ugent.sel.studeez.resources | |||
| fun TaskEntry( | ||||
|     task: Task, | ||||
|     onCheckTask: (Boolean) -> Unit, | ||||
|     onDeleteTask: () -> Unit, | ||||
|     onArchiveTask: () -> Unit, | ||||
|     onStartTask: () -> Unit | ||||
| ) { | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|  | @ -80,7 +71,7 @@ fun TaskEntry( | |||
|             Box(modifier = Modifier.weight(7f)) { | ||||
|                 if (task.completed) { | ||||
|                     IconButton( | ||||
|                         onClick = onDeleteTask, | ||||
|                         onClick = onArchiveTask, | ||||
|                         modifier = Modifier | ||||
|                             .padding(start = 20.dp) | ||||
|                     ) { | ||||
|  | @ -95,6 +86,7 @@ fun TaskEntry( | |||
|                         modifier = Modifier | ||||
|                             .padding(end = 5.dp), | ||||
|                     ) { | ||||
|                         onStartTask() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | @ -110,7 +102,7 @@ fun TaskEntryPreview() { | |||
|             name = "Test Task", | ||||
|             completed = false, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {} | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -122,7 +114,7 @@ fun CompletedTaskEntryPreview() { | |||
|             name = "Test Task", | ||||
|             completed = true, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {}, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -134,6 +126,6 @@ fun OverflowTaskEntryPreview() { | |||
|             name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", | ||||
|             completed = false, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {} | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| package be.ugent.sel.studeez.common.ext | ||||
| 
 | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import kotlin.random.Random | ||||
| 
 | ||||
| fun Color.Companion.generateRandomArgb(): Long { | ||||
|     val random = Random | ||||
|     val mask: Long = (0x000000FFL shl random.nextInt(0, 3)).inv() | ||||
|     return random.nextLong(0xFF000000L, 0xFFFFFFFFL) and mask | ||||
| } | ||||
|  | @ -1,11 +0,0 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class EditTimerState @Inject constructor(){ | ||||
|     lateinit var timerInfo: TimerInfo | ||||
| } | ||||
							
								
								
									
										53
									
								
								app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/src/main/java/be/ugent/sel/studeez/data/SelectedState.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| 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 | ||||
| 
 | ||||
| /** | ||||
|  * Used to cummunicate between viewmodels. | ||||
|  */ | ||||
| abstract class SelectedState<T> { | ||||
|     abstract var value: T | ||||
|     operator fun invoke() = value | ||||
|     fun set(newValue: T) { | ||||
|         this.value = newValue | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| class SelectedSessionReport @Inject constructor() : SelectedState<SessionReport>() { | ||||
|     override lateinit var value: SessionReport | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| class SelectedTask @Inject constructor() : SelectedState<Task>() { | ||||
|     override lateinit var value: Task | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| class SelectedTimer @Inject constructor() : SelectedState<FunctionalTimer>() { | ||||
|     override lateinit var value: FunctionalTimer | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| class SelectedSubject @Inject constructor() : SelectedState<Subject>() { | ||||
|     override lateinit var value: Subject | ||||
| } | ||||
| 
 | ||||
| @Singleton | ||||
| 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() | ||||
| } | ||||
|  | @ -1,20 +0,0 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| /** | ||||
|  * Used to communicate the selected subject from the subject overview other screens. | ||||
|  * Because this is a singleton-class the view-models of both screens observe the same data. | ||||
|  */ | ||||
| @Singleton | ||||
| class SelectedSubject @Inject constructor() { | ||||
|     private lateinit var subject: Subject | ||||
|     operator fun invoke() = subject | ||||
|     fun set(subject: Subject) { | ||||
|         this.subject = subject | ||||
|     } | ||||
| 
 | ||||
|     fun isSet() = this::subject.isInitialized | ||||
| } | ||||
|  | @ -1,21 +0,0 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| /** | ||||
|  * Used to communicate the selected task from the task overview other screens. | ||||
|  * Because this is a singleton-class the view-models of both screens observe the same data. | ||||
|  */ | ||||
| @Singleton | ||||
| class SelectedTask @Inject constructor() { | ||||
|     private lateinit var task: Task | ||||
| 
 | ||||
|     operator fun invoke() = task | ||||
|     fun set(task: Task) { | ||||
|         this.task = task | ||||
|     } | ||||
| 
 | ||||
|     fun isSet() = this::task.isInitialized | ||||
| } | ||||
|  | @ -1,14 +0,0 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| /** | ||||
|  * Used to communicate the SelectedTimer from the selection screen to the session screen. | ||||
|  * Because this is a singleton-class the view-models of both screens observe the same data. | ||||
|  */ | ||||
| @Singleton | ||||
| class SelectedTimerState @Inject constructor(){ | ||||
|     var selectedTimer: FunctionalTimer? = null | ||||
| } | ||||
|  | @ -1,14 +0,0 @@ | |||
| package be.ugent.sel.studeez.data | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| /** | ||||
|  * Used to communicate the SelectedTimer from the selection screen to the session screen. | ||||
|  * Because this is a singleton-class the view-models of both screens observe the same data. | ||||
|  */ | ||||
| @Singleton | ||||
| class SessionReportState @Inject constructor(){ | ||||
|     var sessionReport: SessionReport? = null | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| package be.ugent.sel.studeez.data.local.models | ||||
| 
 | ||||
| import com.google.firebase.Timestamp | ||||
| 
 | ||||
| data class FeedEntry( | ||||
|     val argb_color: Long = 0, | ||||
|     val subJectName: String = "", | ||||
|     val taskName: String = "", | ||||
|     val taskId: String = "", // Name of task is not unique | ||||
|     val subjectId: String = "", | ||||
|     val totalStudyTime: Int = 0, | ||||
|     val endTime: Timestamp = Timestamp(0, 0), | ||||
|     val isArchived: Boolean = false | ||||
| ) | ||||
|  | @ -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 | ||||
| ) | ||||
|  | @ -6,5 +6,7 @@ import com.google.firebase.firestore.DocumentId | |||
| data class SessionReport( | ||||
|     @DocumentId val id: String = "", | ||||
|     val studyTime: Int = 0, | ||||
|     val endTime: Timestamp = Timestamp(0, 0) | ||||
|     val endTime: Timestamp = Timestamp(0, 0), | ||||
|     val taskId: String = "", | ||||
|     val subjectId: String = "" | ||||
| ) | ||||
|  | @ -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 = "" | ||||
| ) | ||||
|  |  | |||
|  | @ -5,6 +5,13 @@ import com.google.firebase.firestore.DocumentId | |||
| data class Subject( | ||||
|     @DocumentId val id: String = "", | ||||
|     val name: String = "", | ||||
|     val time: Int = 0, | ||||
|     val argb_color: Long = 0, | ||||
|     var archived: Boolean = false, | ||||
| ) | ||||
| 
 | ||||
| object SubjectDocument { | ||||
|     const val id = "id" | ||||
|     const val name = "name" | ||||
|     const val archived = "archived" | ||||
|     const val argb_color = "argb_color" | ||||
| } | ||||
|  | @ -5,9 +5,10 @@ import com.google.firebase.firestore.DocumentId | |||
| data class Task( | ||||
|     @DocumentId val id: String = "", | ||||
|     val name: String = "", | ||||
|     val completed: Boolean = false, | ||||
|     var completed: Boolean = false, | ||||
|     val time: Int = 0, | ||||
|     val subjectId: String = "", | ||||
|     var archived: Boolean = false, | ||||
| ) | ||||
| 
 | ||||
| object TaskDocument { | ||||
|  | @ -16,4 +17,5 @@ object TaskDocument { | |||
|     const val completed = "completed" | ||||
|     const val time = "time" | ||||
|     const val subjectId = "subjectId" | ||||
|     const val archived = "archived" | ||||
| } | ||||
|  |  | |||
|  | @ -2,17 +2,17 @@ package be.ugent.sel.studeez.data.local.models.timer_functional | |||
| 
 | ||||
| class FunctionalPomodoroTimer( | ||||
|     private var studyTime: Int, | ||||
|     private var breakTime: Int, repeats: Int | ||||
|     private var breakTime: Int, | ||||
|     val repeats: Int | ||||
| ) : FunctionalTimer(studyTime) { | ||||
| 
 | ||||
|     var breaksRemaining = repeats | ||||
|     var breaksRemaining = repeats - 1 | ||||
|     var isInBreak = false | ||||
| 
 | ||||
|     override fun tick() { | ||||
|         if (hasEnded()) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (hasCurrentCountdownEnded()) { | ||||
|             if (isInBreak) { | ||||
|                 breaksRemaining-- | ||||
|  |  | |||
|  | @ -17,10 +17,12 @@ abstract class FunctionalTimer(initialValue: Int) { | |||
| 
 | ||||
|     abstract fun hasCurrentCountdownEnded(): Boolean | ||||
| 
 | ||||
|     fun getSessionReport(): SessionReport { | ||||
|     fun getSessionReport(subjectId: String, taskId: String): SessionReport { | ||||
|         return SessionReport( | ||||
|             studyTime = totalStudyTime, | ||||
|             endTime = Timestamp.now() | ||||
|             endTime = Timestamp.now(), | ||||
|             taskId = taskId, | ||||
|             subjectId = subjectId | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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,11 +29,14 @@ 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 | ||||
| } | ||||
							
								
								
									
										13
									
								
								app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| 
 | ||||
| interface FeedDAO { | ||||
| 
 | ||||
|     fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> | ||||
| 
 | ||||
|     suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>> | ||||
| 
 | ||||
|     fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>> | ||||
| } | ||||
|  | @ -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,16 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| 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.task.Task | ||||
| 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> | ||||
| 
 | ||||
|     fun saveSession(newSessionReport: SessionReport) | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,4 +12,13 @@ interface SubjectDAO { | |||
|     fun deleteSubject(oldSubject: Subject) | ||||
| 
 | ||||
|     fun updateSubject(newSubject: Subject) | ||||
| 
 | ||||
|     suspend fun archiveSubject(subject: Subject) | ||||
| 
 | ||||
|     fun getTaskCount(subject: Subject): Flow<Int> | ||||
|     fun getCompletedTaskCount(subject: Subject): Flow<Int> | ||||
|     fun getStudyTime(subject: Subject): Flow<Int> | ||||
| 
 | ||||
|     suspend fun getSubject(subjectId: String): Subject? | ||||
|     suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject | ||||
| } | ||||
|  | @ -14,5 +14,7 @@ interface TaskDAO { | |||
| 
 | ||||
|     fun deleteTask(oldTask: Task) | ||||
| 
 | ||||
|     fun toggleTaskCompleted(task: Task, completed: Boolean) | ||||
|     suspend fun getTask(subjectId: String, taskId: String): Task | ||||
| 
 | ||||
|     suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task | ||||
| } | ||||
|  | @ -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,39 +0,0 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.SubjectDAO | ||||
| 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 FireBaseSubjectDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
| ) : SubjectDAO { | ||||
|     override fun getSubjects(): Flow<List<Subject>> { | ||||
|         return currentUserSubjectsCollection() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Subject::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override fun saveSubject(newSubject: Subject) { | ||||
|         currentUserSubjectsCollection().add(newSubject) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteSubject(oldSubject: Subject) { | ||||
|         currentUserSubjectsCollection().document(oldSubject.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     override fun updateSubject(newSubject: Subject) { | ||||
|         currentUserSubjectsCollection().document(newSubject.id).set(newSubject) | ||||
|     } | ||||
| 
 | ||||
|     private fun currentUserSubjectsCollection(): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) | ||||
| } | ||||
|  | @ -1,49 +0,0 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| 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.task.TaskDocument | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| 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 FireBaseTaskDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
| ) : TaskDAO { | ||||
|     override fun getTasks(subject: Subject): Flow<List<Task>> { | ||||
|         return selectedSubjectTasksCollection(subject.id) | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Task::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override fun saveTask(newTask: Task) { | ||||
|         selectedSubjectTasksCollection(newTask.subjectId).add(newTask) | ||||
|     } | ||||
| 
 | ||||
|     override fun updateTask(newTask: Task) { | ||||
|         selectedSubjectTasksCollection(newTask.id).document(newTask.id).set(newTask) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteTask(oldTask: Task) { | ||||
|         selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     override fun toggleTaskCompleted(task: Task, completed: Boolean) { | ||||
|         selectedSubjectTasksCollection(task.subjectId) | ||||
|             .document(task.id) | ||||
|             .update(TaskDocument.completed, completed) | ||||
|     } | ||||
| 
 | ||||
|     private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference = | ||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(FireBaseCollections.SUBJECT_COLLECTION) | ||||
|             .document(subjectId) | ||||
|             .collection(FireBaseCollections.TASK_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,137 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import android.icu.text.DateFormat | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| 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.domain.* | ||||
| import com.google.firebase.Timestamp | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FirebaseFeedDAO @Inject constructor( | ||||
|     private val friendshipDAO: FriendshipDAO, | ||||
|     private val sessionDAO: SessionDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|     private val subjectDAO: SubjectDAO, | ||||
|     private val auth: AccountDAO, | ||||
|     private val userDAO: UserDAO, | ||||
| ) : FeedDAO { | ||||
| 
 | ||||
|     /** | ||||
|      *  Return a map as with key the day and value a list of feedentries for that day. | ||||
|      */ | ||||
|     override fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> { | ||||
|         return sessionDAO.getSessions().map { sessionReports -> | ||||
|             sessionReports | ||||
|                 .map { sessionReport -> sessionToFeedEntry(sessionReport) } | ||||
|                 .sortedByDescending { it.endTime } | ||||
|                 .groupBy { getFormattedTime(it) } | ||||
|                 .mapValues { (_, entries) -> | ||||
|                     entries | ||||
|                         .groupBy { it.taskId } | ||||
|                         .map { fuseFeedEntries(it.component2()) } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Return a map as with key the day and value a list of feedentries for that day. | ||||
|      */ | ||||
|     override suspend fun getFeedEntriesFromUser(id: String): Map<String, List<FeedEntry>> { | ||||
|         return sessionDAO.getSessionsOfUser(id) | ||||
|                 .map { sessionReport -> sessionToFeedEntryFromUser(sessionReport, id) } | ||||
|                 .sortedByDescending { it.endTime } | ||||
|                 .groupBy { getFormattedTime(it) } | ||||
|                 .mapValues { (_, entries) -> | ||||
|                     entries | ||||
|                         .groupBy { it.taskId } | ||||
|                         .map { fuseFeedEntries(it.component2()) } | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
|     override fun getFriendsSessions(): Flow<Map<String, List<Pair<String, FeedEntry>>>> { | ||||
|         return friendshipDAO.getAllFriendships(auth.currentUserId) | ||||
|             .map { friendships -> | ||||
|                 friendships.map { friendship -> | ||||
|                     val userId: String = friendship.friendId | ||||
|                     val username = userDAO.getUsername(userId) | ||||
|                     val friendFeed = getFeedEntriesFromUser(userId) | ||||
|                     Pair(username, friendFeed) | ||||
|                 } | ||||
|             }.map { | ||||
|                 mergeNameAndEntries(it) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     private fun mergeNameAndEntries(l: List<Pair<String, Map<String, List<FeedEntry>>>>): Map<String, List<Pair<String, FeedEntry>>> { | ||||
|         val new: MutableMap<String, List<Pair<String, FeedEntry>>> = mutableMapOf() | ||||
|         for ((name, map) in l) { | ||||
|             for ((day, feedEntries: List<FeedEntry>) in map) { | ||||
|                 new[day] = new.getOrDefault(day, listOf()) +  feedEntries.map { Pair(name, it) } | ||||
|             } | ||||
|         } | ||||
|         return new | ||||
|     } | ||||
| 
 | ||||
|     private fun getFormattedTime(entry: FeedEntry): String { | ||||
|         return DateFormat.getDateInstance().format(entry.endTime.toDate()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Givin a list of entries referencing the same task, in the same day, fuse them into one | ||||
|      * feed-entry by adding the studytime and keeping the most recent end-timestamp | ||||
|      */ | ||||
|     private fun fuseFeedEntries(entries: List<FeedEntry>): FeedEntry = | ||||
|         entries.drop(1).fold(entries[0]) { accEntry, newEntry -> | ||||
|             accEntry.copy( | ||||
|                 totalStudyTime = accEntry.totalStudyTime + newEntry.totalStudyTime, | ||||
|                 endTime = getMostRecent(accEntry.endTime, newEntry.endTime) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     private fun getMostRecent(t1: Timestamp, t2: Timestamp): Timestamp { | ||||
|         return if (t1 < t2) t2 else t1 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names | ||||
|      */ | ||||
|     private suspend fun sessionToFeedEntry(sessionReport: SessionReport): FeedEntry { | ||||
|         val subjectId: String = sessionReport.subjectId | ||||
|         val taskId: String = sessionReport.taskId | ||||
| 
 | ||||
|         val task: Task = taskDAO.getTask(subjectId, taskId) | ||||
|         val subject: Subject = subjectDAO.getSubject(subjectId)!! | ||||
| 
 | ||||
|         return makeFeedEntry(sessionReport, subject, task) | ||||
|     } | ||||
| 
 | ||||
|     private fun makeFeedEntry(sessionReport: SessionReport, subject: Subject, task: Task): FeedEntry { | ||||
|         return FeedEntry( | ||||
|             argb_color = subject.argb_color, | ||||
|             subJectName = subject.name, | ||||
|             taskName = task.name, | ||||
|             taskId = task.id, | ||||
|             subjectId = subject.id, | ||||
|             totalStudyTime = sessionReport.studyTime, | ||||
|             endTime = sessionReport.endTime, | ||||
|             isArchived = task.archived || subject.archived | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names | ||||
|      */ | ||||
|     private suspend fun sessionToFeedEntryFromUser(sessionReport: SessionReport, id: String): FeedEntry { | ||||
|         val subjectId: String = sessionReport.subjectId | ||||
|         val taskId: String = sessionReport.taskId | ||||
| 
 | ||||
|         val task: Task = taskDAO.getTaskFromUser(subjectId, taskId, id) | ||||
|         val subject: Subject = subjectDAO.getSubjectOfUSer(subjectId, id) | ||||
| 
 | ||||
|         return makeFeedEntry(sessionReport, subject, task) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,150 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| 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.FRIENDID | ||||
| import be.ugent.sel.studeez.data.remote.FirebaseFriendship.FRIENDSSINCE | ||||
| 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 kotlinx.coroutines.tasks.await | ||||
| 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 | ||||
| 
 | ||||
|         // Check if the friendship already exists for the logged in user | ||||
|         var allowed = false | ||||
|         firestore.collection(USER_COLLECTION) | ||||
|             .document(currentUserId) | ||||
|             .collection(FRIENDS_COLLECTION) | ||||
|             .whereEqualTo(FRIENDID, otherUserId) | ||||
|             .get() | ||||
|             .addOnSuccessListener { | ||||
|                 allowed = it.documents.isEmpty() | ||||
| 
 | ||||
|                 if (allowed) { | ||||
|                     // 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() | ||||
|                         )) | ||||
|                 } | ||||
|             }.addOnSuccessListener { | ||||
|                 val message = if (allowed) AppText.success else AppText.already_friend | ||||
|                 SnackbarManager.showMessage(message) | ||||
|             } | ||||
| 
 | ||||
|         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,58 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| 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.task.Task | ||||
| 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.* | ||||
| 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 com.google.firebase.firestore.ktx.toObject | ||||
| 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, | ||||
| ) : SessionDAO { | ||||
| 
 | ||||
|     override fun getSessions(): Flow<List<SessionReport>> { | ||||
|         return currentUserSessionsCollection() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(SessionReport::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSessionsOfUser(userId: String): List<SessionReport> { | ||||
|         return firestore.collection(USER_COLLECTION) | ||||
|             .document(userId) | ||||
|             .collection(SESSION_COLLECTION) | ||||
|             .get().await() | ||||
|             .map { it.toObject(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(USER_COLLECTION) | ||||
|             .document(auth.currentUserId) | ||||
|             .collection(SESSION_COLLECTION) | ||||
| } | ||||
|  | @ -0,0 +1,97 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.task.SubjectDocument | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.data.local.models.task.TaskDocument | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.SubjectDAO | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.Query | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import com.google.firebase.firestore.ktx.toObject | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| import kotlin.collections.count | ||||
| 
 | ||||
| class FirebaseSubjectDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
| ) : SubjectDAO { | ||||
|     override fun getSubjects(): Flow<List<Subject>> { | ||||
|         return currentUserSubjectsCollection() | ||||
|             .subjectNotArchived() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Subject::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSubject(subjectId: String): Subject? { | ||||
|         return currentUserSubjectsCollection().document(subjectId).get().await().toObject() | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSubjectOfUSer(subjectId: String, userId: String): Subject { | ||||
|         return currentUserSubjectsCollection(userId).document(subjectId).get().await().toObject()!! | ||||
|     } | ||||
| 
 | ||||
|     override fun saveSubject(newSubject: Subject) { | ||||
|         currentUserSubjectsCollection().add(newSubject) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteSubject(oldSubject: Subject) { | ||||
|         currentUserSubjectsCollection().document(oldSubject.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     override fun updateSubject(newSubject: Subject) { | ||||
|         currentUserSubjectsCollection().document(newSubject.id).set(newSubject) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun archiveSubject(subject: Subject) { | ||||
|         currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) | ||||
|         currentUserSubjectsCollection().document(subject.id) | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
|             .taskNotArchived() | ||||
|             .get().await() | ||||
|             .documents | ||||
|             .forEach { | ||||
|                 it.reference.update(TaskDocument.archived, true) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     override fun getTaskCount(subject: Subject): Flow<Int> { | ||||
|         return taskDAO.getTasks(subject) | ||||
|             .map(List<Task>::count) | ||||
|     } | ||||
| 
 | ||||
|     override fun getCompletedTaskCount(subject: Subject): Flow<Int> { | ||||
|         return taskDAO.getTasks(subject) | ||||
|             .map { tasks -> tasks.count { it.completed && !it.archived } } | ||||
|     } | ||||
| 
 | ||||
|     override fun getStudyTime(subject: Subject): Flow<Int> { | ||||
|         return taskDAO.getTasks(subject) | ||||
|             .map { tasks -> tasks.sumOf { it.time } } | ||||
|     } | ||||
| 
 | ||||
|     private fun currentUserSubjectsCollection(id: String = auth.currentUserId): CollectionReference = | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(id) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
| 
 | ||||
|     private fun subjectTasksCollection(subject: Subject, id: String = auth.currentUserId): CollectionReference = | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(id) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
|             .document(subject.id) | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
| 
 | ||||
|     fun CollectionReference.subjectNotArchived(): Query = | ||||
|         this.whereEqualTo(SubjectDocument.archived, false) | ||||
| 
 | ||||
|     fun Query.subjectNotArchived(): Query = | ||||
|         this.whereEqualTo(SubjectDocument.archived, false) | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| 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.task.TaskDocument | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.Query | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import com.google.firebase.firestore.ktx.toObject | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FirebaseTaskDAO @Inject constructor( | ||||
|     private val firestore: FirebaseFirestore, | ||||
|     private val auth: AccountDAO, | ||||
| ) : TaskDAO { | ||||
|     override fun getTasks(subject: Subject): Flow<List<Task>> { | ||||
|         return selectedSubjectTasksCollection(subject.id) | ||||
|             .taskNotArchived() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Task::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getTask(subjectId: String, taskId: String): Task { | ||||
|         return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!! | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getTaskFromUser(subjectId: String, taskId: String, userId: String): Task { | ||||
|         return selectedSubjectTasksCollection(subjectId, userId) | ||||
|             .document(taskId) | ||||
|             .get() | ||||
|             .await().toObject(Task::class.java)!! | ||||
|     } | ||||
| 
 | ||||
|     override fun saveTask(newTask: Task) { | ||||
|         selectedSubjectTasksCollection(newTask.subjectId).add(newTask) | ||||
|     } | ||||
| 
 | ||||
|     override fun updateTask(newTask: Task) { | ||||
|         selectedSubjectTasksCollection(newTask.subjectId) | ||||
|             .document(newTask.id) | ||||
|             .set(newTask) | ||||
|     } | ||||
| 
 | ||||
|     override fun deleteTask(oldTask: Task) { | ||||
|         selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete() | ||||
|     } | ||||
| 
 | ||||
|     private fun selectedSubjectTasksCollection(subjectId: String, id: String = auth.currentUserId): CollectionReference = | ||||
|         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||
|             .document(id) | ||||
|             .collection(FirebaseCollections.SUBJECT_COLLECTION) | ||||
|             .document(subjectId) | ||||
|             .collection(FirebaseCollections.TASK_COLLECTION) | ||||
| } | ||||
| 
 | ||||
| // Extend CollectionReference and Query with some filters | ||||
| 
 | ||||
| fun CollectionReference.taskNotArchived(): Query = | ||||
|     this.whereEqualTo(TaskDocument.archived, false) | ||||
| 
 | ||||
| fun Query.taskNotArchived(): Query = | ||||
|     this.whereEqualTo(TaskDocument.archived, false) | ||||
| 
 | ||||
| fun CollectionReference.taskNotCompleted(): Query = | ||||
|     this.whereEqualTo(TaskDocument.completed, true) | ||||
| 
 | ||||
| fun Query.taskNotCompleted(): Query = | ||||
|     this.whereEqualTo(TaskDocument.completed, true) | ||||
|  | @ -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) } | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ object StudeezDestinations { | |||
|     // NavBar | ||||
|     const val HOME_SCREEN = "home" | ||||
|     const val SUBJECT_SCREEN = "subjects" | ||||
|     const val SESSIONS_SCREEN = "sessions" | ||||
|     const val FRIENDS_FEED = "friends_feed" | ||||
|     const val PROFILE_SCREEN = "profile" | ||||
| 
 | ||||
|     // Drawer | ||||
|  | @ -27,10 +27,13 @@ object StudeezDestinations { | |||
|     const val EDIT_SUBJECT_FORM = "edit_subject" | ||||
|     const val TASKS_SCREEN = "tasks" | ||||
|     const val ADD_TASK_FORM = "add_task" | ||||
|     const val SELECT_SUBJECT = "select_subject" | ||||
|     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,22 +14,26 @@ 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.friends_feed.FriendsFeedRoute | ||||
| 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.ProfileRoute | ||||
| import be.ugent.sel.studeez.screens.profile.edit_profile.EditProfileRoute | ||||
| 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 | ||||
| import be.ugent.sel.studeez.screens.settings.SettingsRoute | ||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpRoute | ||||
| import be.ugent.sel.studeez.screens.splash.SplashRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.SubjectRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.SubjectRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectCreateRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectEditRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.select.SubjectSelectionRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.TaskRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.forms.SubjectAddRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.forms.SubjectEditRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.forms.TaskAddRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.forms.TaskEditRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute | ||||
| import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute | ||||
| import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute | ||||
| import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen | ||||
|  | @ -51,6 +55,7 @@ fun StudeezNavGraph( | |||
|     val open: (String) -> Unit = { appState.navigate(it) } | ||||
|     val openAndPopUp: (String, String) -> Unit = | ||||
|         { route, popUp -> appState.navigateAndPopUp(route, popUp) } | ||||
|     val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) } | ||||
| 
 | ||||
|     val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) | ||||
|     val navigationBarActions: NavigationBarActions = | ||||
|  | @ -64,10 +69,11 @@ fun StudeezNavGraph( | |||
|         // NavBar | ||||
|         composable(StudeezDestinations.HOME_SCREEN) { | ||||
|             HomeRoute( | ||||
|                 open, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 open = open, | ||||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|                 feedViewModel = hiltViewModel(), | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  | @ -80,8 +86,16 @@ fun StudeezNavGraph( | |||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.SELECT_SUBJECT) { | ||||
|             SubjectSelectionRoute( | ||||
|                 open = { openAndPopUp(it, StudeezDestinations.SELECT_SUBJECT) }, | ||||
|                 goBack = goBack, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.ADD_SUBJECT_FORM) { | ||||
|             SubjectAddRoute( | ||||
|             SubjectCreateRoute( | ||||
|                 goBack = goBack, | ||||
|                 openAndPopUp = openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|  | @ -98,14 +112,14 @@ fun StudeezNavGraph( | |||
| 
 | ||||
|         composable(StudeezDestinations.TASKS_SCREEN) { | ||||
|             TaskRoute( | ||||
|                 goBack = goBack, | ||||
|                 goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) }, | ||||
|                 open = open, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.ADD_TASK_FORM) { | ||||
|             TaskAddRoute( | ||||
|             TaskCreateRoute( | ||||
|                 goBack = goBack, | ||||
|                 openAndPopUp = openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|  | @ -121,10 +135,11 @@ fun StudeezNavGraph( | |||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         composable(StudeezDestinations.SESSIONS_SCREEN) { | ||||
|             SessionsRoute( | ||||
|         composable(StudeezDestinations.FRIENDS_FEED) { | ||||
|             FriendsFeedRoute( | ||||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  | @ -200,7 +215,7 @@ fun StudeezNavGraph( | |||
| 
 | ||||
|         composable(StudeezDestinations.SESSION_RECAP) { | ||||
|             SessionRecapRoute( | ||||
|                 openAndPopUp = openAndPopUp, | ||||
|                 clearAndNavigate = clearAndNavigate, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
|  | @ -220,8 +235,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,308 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_overview | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| 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.material.icons.filled.Search | ||||
| 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.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 Make search field | ||||
| //                    SearchField( | ||||
| //                        value = uiState.queryString, | ||||
| //                        onValueChange = friendsOverviewActions.onQueryStringChange, | ||||
| //                        onSubmit = friendsOverviewActions.onSubmit, | ||||
| //                        label = AppText.search_friends, | ||||
| //                        enabled = false | ||||
| //                    ) | ||||
|                         IconButton( | ||||
|                             onClick = friendsOverviewActions.onSubmit, | ||||
| //                            modifier = Modifier.background( | ||||
| //                                color = MaterialTheme.colors.background | ||||
| //                            ), | ||||
|                         ) { | ||||
|                             Row { | ||||
|                                 Text( | ||||
|                                     text = stringResource(id = AppText.click_search_friends), | ||||
|                                     color = MaterialTheme.colors.onPrimary | ||||
|                                 ) | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Default.Search, | ||||
|                                     contentDescription = stringResource(AppText.search_friends), | ||||
|                                     tint = MaterialTheme.colors.onPrimary | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                 }, | ||||
|                 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 | ||||
| ) { | ||||
|     Card { | ||||
|         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 | ||||
|                 ) { | ||||
|                     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,264 @@ | |||
| package be.ugent.sel.studeez.screens.friends.friends_search | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| 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.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 = { | ||||
|                     // TODO Make search field | ||||
| //                    SearchField( | ||||
| //                        value = uiState.queryString, | ||||
| //                        onValueChange = friendsOverviewActions.onQueryStringChange, | ||||
| //                        onSubmit = friendsOverviewActions.onSubmit, | ||||
| //                        label = AppText.search_friends, | ||||
| //                        enabled = false | ||||
| //                    ) | ||||
|                     Text( | ||||
|                         text = stringResource(id = AppText.searching_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.map | ||||
| 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() | ||||
|             .map { users -> | ||||
|                 users.filter { user -> | ||||
|                     user.id != userDAO.getCurrentUserId() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     fun goToProfile( | ||||
|         userId: String, | ||||
|         open: (String) -> Unit | ||||
|     ) { | ||||
|         selectedProfileState.value = userId | ||||
|         open(StudeezDestinations.PUBLIC_PROFILE_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -9,7 +9,6 @@ import androidx.compose.material.Card | |||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
|  | @ -20,7 +19,6 @@ import androidx.compose.ui.unit.dp | |||
| import be.ugent.sel.studeez.common.composable.DateText | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.feed.LoadingFeed | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
|  | @ -33,11 +31,10 @@ fun FriendsFeedRoute( | |||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions | ||||
| ) { | ||||
|     val friendsFeedUiState by viewModel.uiState.collectAsState() | ||||
|     FriendsFeedScreen( | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         uiState = friendsFeedUiState, | ||||
|         viewModel = viewModel | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -45,20 +42,22 @@ fun FriendsFeedRoute( | |||
| fun FriendsFeedScreen( | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     uiState: FriendsFeedUiState, | ||||
|     viewModel: FriendsFeedViewModel | ||||
| ) { | ||||
|     PrimaryScreenTemplate( | ||||
|         title = resources().getString(AppText.friends_feed), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions | ||||
|     ) { | ||||
|         when (uiState) { | ||||
|             FriendsFeedUiState.Loading -> LoadingFeed() | ||||
|             is FriendsFeedUiState.Succes -> { | ||||
|                 val friendsSessions = uiState.friendSessions | ||||
| 
 | ||||
|         val friendsSessions = viewModel.getFriendsSessions().collectAsState(initial = emptyList()) | ||||
|              | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         LazyColumn() { | ||||
|             // Default Timers, cannot be edited | ||||
|                     items(friendsSessions) { | ||||
|             items(friendsSessions.value) { | ||||
|                 val (day, feedEntries) = it | ||||
|                 DateText(date = day) | ||||
|                 feedEntries.forEach { (name, feedEntry) -> | ||||
|  | @ -67,8 +66,7 @@ fun FriendsFeedScreen( | |||
|                 Spacer(modifier = Modifier.height(10.dp)) | ||||
|             } | ||||
|         } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,14 +1,15 @@ | |||
| package be.ugent.sel.studeez.screens.friends_feed | ||||
| 
 | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.domain.FeedDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.SessionDAO | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.SharingStarted | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.stateIn | ||||
| import kotlinx.coroutines.flow.toList | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
|  | @ -17,13 +18,10 @@ class FriendsFeedViewModel @Inject constructor( | |||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     val uiState: StateFlow<FriendsFeedUiState> = feedDAO.getFriendsSessions() | ||||
|         .map { it.toList() } | ||||
|         .map { FriendsFeedUiState.Succes(it) } | ||||
|         .stateIn( | ||||
|             scope = viewModelScope, | ||||
|             initialValue = FriendsFeedUiState.Loading, | ||||
|             started = SharingStarted.Eagerly, | ||||
|         ) | ||||
|     fun getFriendsSessions(): Flow<List<Pair<String, List<Pair<String, FeedEntry>>>>> { | ||||
|         return feedDAO.getFriendsSessions().map { it.toList() } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,14 +5,17 @@ import androidx.compose.material.IconButton | |||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.feed.Feed | ||||
| import be.ugent.sel.studeez.common.composable.feed.FeedUiState | ||||
| import be.ugent.sel.studeez.common.composable.feed.FeedViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.resources | ||||
| 
 | ||||
| @Composable | ||||
|  | @ -21,35 +24,43 @@ fun HomeRoute( | |||
|     viewModel: HomeViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedViewModel: FeedViewModel, | ||||
| ) { | ||||
|     val feedUiState by feedViewModel.uiState.collectAsState() | ||||
|     HomeScreen( | ||||
|         onStartSessionClick = { viewModel.onStartSessionClick(open) }, | ||||
|         onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         feedUiState = feedUiState, | ||||
|         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, | ||||
|         onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun HomeScreen( | ||||
|     onStartSessionClick: () -> Unit, | ||||
|     onViewFriendsClick: () -> Unit, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedUiState: FeedUiState, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit, | ||||
| ) { | ||||
|     PrimaryScreenTemplate( | ||||
|         title = resources().getString(R.string.home), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         // TODO barAction = { FriendsAction() } | ||||
|         barAction = { FriendsAction(onViewFriendsClick) } | ||||
|     ) { | ||||
|         BasicButton(R.string.start_session, Modifier.basicButton()) { | ||||
|             onStartSessionClick() | ||||
|         } | ||||
|         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) | ||||
|  | @ -61,8 +72,40 @@ fun FriendsAction() { | |||
| @Composable | ||||
| fun HomeScreenPreview() { | ||||
|     HomeScreen( | ||||
|         onStartSessionClick = {}, | ||||
|         onViewFriendsClick = {}, | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||
|         feedUiState = FeedUiState.Succes( | ||||
|             mapOf( | ||||
|                 "08 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFABD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 600, | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 20, | ||||
|                     ), | ||||
|                 ), | ||||
|                 "09 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFD1200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFF5C89, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                 ) | ||||
|             ) | ||||
|         ), | ||||
|         continueTask = { _, _ -> run {} }, | ||||
|         onEmptyFeedHelp = {} | ||||
|     ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| package be.ugent.sel.studeez.screens.home | ||||
| 
 | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
|  | @ -9,11 +7,11 @@ import javax.inject.Inject | |||
| 
 | ||||
| @HiltViewModel | ||||
| class HomeViewModel @Inject constructor( | ||||
|     private val accountDAO: AccountDAO, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     fun onStartSessionClick(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.TIMER_SELECTION_SCREEN) | ||||
| 
 | ||||
|     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,39 @@ | |||
| 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 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,179 @@ | |||
| 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: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| 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 = {} | ||||
|             ), | ||||
|             popUp = {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun PublicProfileEllipsis( | ||||
|     sendFriendRequest: () -> 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) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     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 = {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|     ) { | ||||
|         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() | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.SessionReportState | ||||
| 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.FunctionalTimer | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
|  | @ -11,23 +12,21 @@ import javax.inject.Inject | |||
| 
 | ||||
| @HiltViewModel | ||||
| class SessionViewModel @Inject constructor( | ||||
|     private val selectedTimerState: SelectedTimerState, | ||||
|     private val sessionReportState: SessionReportState, | ||||
|     private val selectedTimer: SelectedTimer, | ||||
|     private val sessionReport: SelectedSessionReport, | ||||
|     private val selectedTask: SelectedTask, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     private val task : String = "No task selected" // placeholder for tasks implementation | ||||
| 
 | ||||
|     fun getTimer(): FunctionalTimer { | ||||
|         return selectedTimerState.selectedTimer!! | ||||
|         return selectedTimer() | ||||
|     } | ||||
| 
 | ||||
|     fun getTask(): String { | ||||
|         return task | ||||
|         return selectedTask().name | ||||
|     } | ||||
| 
 | ||||
|     fun endSession(openAndPopUp: (String, String) -> Unit) { | ||||
|         sessionReportState.sessionReport = getTimer().getSessionReport() | ||||
|         sessionReport.set(getTimer().getSessionReport(selectedTask().subjectId, selectedTask().id)) | ||||
|         openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -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,143 +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 | ||||
|             ) | ||||
| 
 | ||||
|             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 | ||||
| 
 | ||||
|     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,45 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import androidx.compose.runtime.Composable | ||||
| 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.R.string as AppText | ||||
| 
 | ||||
| class BreakSessionScreen( | ||||
|     private val funPomoDoroTimer: FunctionalPomodoroTimer, | ||||
|     private var mediaplayer: MediaPlayer? | ||||
| ): AbstractSessionScreen() { | ||||
| 
 | ||||
|     @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().getQuantityString( | ||||
|             R.plurals.state_focus_remaining, | ||||
|             funPomoDoroTimer.breaksRemaining, | ||||
|             funPomoDoroTimer.breaksRemaining | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() { | ||||
|         if (funPomoDoroTimer.hasEnded()) { | ||||
|             mediaplayer?.let { it: MediaPlayer -> | ||||
|                 it.setOnCompletionListener { | ||||
|                     it.release() | ||||
|                     mediaplayer = null | ||||
|                 } | ||||
|                 it.start() | ||||
|             } | ||||
|         } else if (funPomoDoroTimer.hasCurrentCountdownEnded()) { | ||||
|             mediaplayer?.start() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show more
		Reference in a new issue
	
	 brreynie
						brreynie