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"> | <project version="4"> | ||||||
|   <component name="ExternalStorageConfigurationManager" enabled="true" /> |   <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" /> |     <output url="file://$PROJECT_DIR$/build/classes" /> | ||||||
|   </component> |   </component> | ||||||
|   <component name="ProjectType"> |   <component name="ProjectType"> | ||||||
|  |  | ||||||
|  | @ -123,9 +123,6 @@ dependencies { | ||||||
|     implementation 'com.google.firebase:firebase-firestore-ktx' |     implementation 'com.google.firebase:firebase-firestore-ktx' | ||||||
|     implementation 'com.google.firebase:firebase-perf-ktx' |     implementation 'com.google.firebase:firebase-perf-ktx' | ||||||
|     implementation 'com.google.firebase:firebase-config-ktx' |     implementation 'com.google.firebase:firebase-config-ktx' | ||||||
| 
 |  | ||||||
|     // Colorpicker |  | ||||||
|     implementation 'com.github.skydoves:colorpicker-compose:1.0.2' |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Allow references to generate code | // 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). |  * See [testing documentation](http://d.android.com/tools/testing). | ||||||
|  */ |  */ | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class ExampleInstrumentedTest { | class InstrumentedTest { | ||||||
|     @Test |     @Test | ||||||
|     fun useAppContext() { |     fun useAppContext() { | ||||||
|         // Context of the app under test. |         // 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.annotation.StringRes | ||||||
| import androidx.compose.foundation.BorderStroke | import androidx.compose.foundation.BorderStroke | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.* | ||||||
| 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.shape.RoundedCornerShape | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
| import androidx.compose.material.Button | import androidx.compose.material.* | ||||||
| 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.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Add | import androidx.compose.material.icons.filled.Add | ||||||
| import androidx.compose.runtime.Composable | 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 | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | fun BasicTextButton( | ||||||
|  |     @StringRes text: Int, | ||||||
|  |     modifier: Modifier, | ||||||
|  |     action: () -> Unit | ||||||
|  | ) { | ||||||
|     TextButton( |     TextButton( | ||||||
|         onClick = action, |         onClick = action, | ||||||
|         modifier = modifier |         modifier = modifier | ||||||
|  | @ -48,6 +42,7 @@ fun BasicButton( | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
|     colors: ButtonColors = ButtonDefaults.buttonColors(), |     colors: ButtonColors = ButtonDefaults.buttonColors(), | ||||||
|     border: BorderStroke? = null, |     border: BorderStroke? = null, | ||||||
|  |     enabled: Boolean = true, | ||||||
|     onClick: () -> Unit, |     onClick: () -> Unit, | ||||||
| ) { | ) { | ||||||
|     Button( |     Button( | ||||||
|  | @ -56,6 +51,7 @@ fun BasicButton( | ||||||
|         shape = defaultButtonShape(), |         shape = defaultButtonShape(), | ||||||
|         colors = colors, |         colors = colors, | ||||||
|         border = border, |         border = border, | ||||||
|  |         enabled = enabled, | ||||||
|     ) { |     ) { | ||||||
|         Text( |         Text( | ||||||
|             text = stringResource(text), |             text = stringResource(text), | ||||||
|  | @ -74,17 +70,22 @@ fun BasicButtonPreview() { | ||||||
| fun StealthButton( | fun StealthButton( | ||||||
|     @StringRes text: Int, |     @StringRes text: Int, | ||||||
|     modifier: Modifier = Modifier.card(), |     modifier: Modifier = Modifier.card(), | ||||||
|  |     enabled: Boolean = true, | ||||||
|     onClick: () -> Unit, |     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( |     BasicButton( | ||||||
|         text = text, |         text = text, | ||||||
|         onClick = onClick, |         onClick = onClick, | ||||||
|         modifier = modifier, |         modifier = modifier, | ||||||
|  |         enabled = enabled, | ||||||
|         colors = ButtonDefaults.buttonColors( |         colors = ButtonDefaults.buttonColors( | ||||||
|             backgroundColor = MaterialTheme.colors.surface, |             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.animateFloat | ||||||
| import androidx.compose.animation.core.updateTransition | import androidx.compose.animation.core.updateTransition | ||||||
| import androidx.compose.foundation.border |  | ||||||
| import androidx.compose.foundation.layout.* | import androidx.compose.foundation.layout.* | ||||||
| import androidx.compose.material.FloatingActionButton | import androidx.compose.material.FloatingActionButton | ||||||
| import androidx.compose.material.Icon | 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.Arrangement | ||||||
| import androidx.compose.foundation.layout.Row | import androidx.compose.foundation.layout.Row | ||||||
| import androidx.compose.foundation.layout.fillMaxWidth | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
| import androidx.compose.material.Text | import androidx.compose.material.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.ui.Alignment | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.text.font.FontWeight | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.unit.sp | import androidx.compose.ui.unit.sp | ||||||
| 
 | 
 | ||||||
| @Composable | @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.annotation.StringRes | ||||||
| import androidx.compose.foundation.layout.Column | import androidx.compose.foundation.layout.Column | ||||||
| import androidx.compose.foundation.layout.padding | import androidx.compose.foundation.layout.padding | ||||||
| import androidx.compose.foundation.text.KeyboardActions |  | ||||||
| import androidx.compose.foundation.text.KeyboardOptions | import androidx.compose.foundation.text.KeyboardOptions | ||||||
| import androidx.compose.material.* | import androidx.compose.material.* | ||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Email | import androidx.compose.material.icons.filled.Email | ||||||
| import androidx.compose.material.icons.filled.Lock | import androidx.compose.material.icons.filled.Lock | ||||||
| import androidx.compose.material.icons.filled.Person | import androidx.compose.material.icons.filled.Person | ||||||
|  | import androidx.compose.material.icons.filled.Search | ||||||
| import androidx.compose.runtime.* | import androidx.compose.runtime.* | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.res.painterResource | import androidx.compose.ui.res.painterResource | ||||||
|  | @ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | import be.ugent.sel.studeez.common.ext.fieldModifier | ||||||
| import be.ugent.sel.studeez.resources | 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.drawable as AppIcon | ||||||
| import be.ugent.sel.studeez.R.string as AppText | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
|  | @ -47,7 +46,7 @@ fun LabelledInputField( | ||||||
|     value: String, |     value: String, | ||||||
|     onNewValue: (String) -> Unit, |     onNewValue: (String) -> Unit, | ||||||
|     @StringRes label: Int, |     @StringRes label: Int, | ||||||
|     singleLine: Boolean = false |     singleLine: Boolean = true | ||||||
| ) { | ) { | ||||||
|     OutlinedTextField( |     OutlinedTextField( | ||||||
|         value = value, |         value = value, | ||||||
|  | @ -119,7 +118,9 @@ fun LabeledErrorTextField( | ||||||
|     initialValue: String, |     initialValue: String, | ||||||
|     @StringRes label: Int, |     @StringRes label: Int, | ||||||
|     singleLine: Boolean = false, |     singleLine: Boolean = false, | ||||||
|     errorText: Int, |     isValid: MutableState<Boolean> = remember { mutableStateOf(true) }, | ||||||
|  |     isFirst: MutableState<Boolean> = remember { mutableStateOf(false) }, | ||||||
|  |     @StringRes errorText: Int, | ||||||
|     keyboardType: KeyboardType, |     keyboardType: KeyboardType, | ||||||
|     predicate: (String) -> Boolean, |     predicate: (String) -> Boolean, | ||||||
|     onNewCorrectValue: (String) -> Unit |     onNewCorrectValue: (String) -> Unit | ||||||
|  | @ -128,31 +129,28 @@ fun LabeledErrorTextField( | ||||||
|         mutableStateOf(initialValue) |         mutableStateOf(initialValue) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var isValid by remember { |  | ||||||
|         mutableStateOf(predicate(value)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Column { |     Column { | ||||||
|         OutlinedTextField( |         OutlinedTextField( | ||||||
|             modifier = modifier.fieldModifier(), |             modifier = modifier.fieldModifier(), | ||||||
|             value = value, |             value = value, | ||||||
|             onValueChange = { newText -> |             onValueChange = { newText -> | ||||||
|  |                 isFirst.value = false | ||||||
|                 value = newText |                 value = newText | ||||||
|                 isValid = predicate(value) |                 isValid.value = predicate(value) | ||||||
|                 if (isValid) { |                 if (isValid.value) { | ||||||
|                     onNewCorrectValue(newText) |                     onNewCorrectValue(newText) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             singleLine = singleLine, |             singleLine = singleLine, | ||||||
|             label = { Text(text = stringResource(id = label)) }, |             label = { Text(text = stringResource(id = label)) }, | ||||||
|             isError = !isValid, |             isError = !isValid.value && !isFirst.value, | ||||||
|             keyboardOptions = KeyboardOptions( |             keyboardOptions = KeyboardOptions( | ||||||
|                 keyboardType = keyboardType, |                 keyboardType = keyboardType, | ||||||
|                 imeAction = ImeAction.Done |                 imeAction = ImeAction.Done | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         if (!isValid) { |         if (!isValid.value && !isFirst.value) { | ||||||
|             Text( |             Text( | ||||||
|                 modifier = Modifier.padding(start = 16.dp), |                 modifier = Modifier.padding(start = 16.dp), | ||||||
|                 text = stringResource(id = errorText), |                 text = stringResource(id = errorText), | ||||||
|  | @ -219,3 +217,35 @@ private fun PasswordField( | ||||||
|         visualTransformation = visualTransformation |         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.Check | ||||||
| import androidx.compose.material.icons.filled.List | import androidx.compose.material.icons.filled.List | ||||||
| import androidx.compose.material.icons.filled.Person | 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.DateRange | ||||||
|  | import androidx.compose.material.icons.outlined.Face | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | 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.HOME_SCREEN | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_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.navigation.StudeezDestinations.SUBJECT_SCREEN | ||||||
| import be.ugent.sel.studeez.resources | import be.ugent.sel.studeez.resources | ||||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
|  | @ -99,11 +101,11 @@ fun NavigationBar( | ||||||
|         BottomNavigationItem( |         BottomNavigationItem( | ||||||
|             icon = { |             icon = { | ||||||
|                 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)) }, |             label = { Text(text = resources().getString(AppText.friends_feed)) }, | ||||||
|             selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN), |             selected = navigationBarActions.isSelectedTab(FRIENDS_FEED), | ||||||
|             onClick = navigationBarActions.onSessionsClick |             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.common.snackbar.SnackbarManager | ||||||
| import be.ugent.sel.studeez.domain.LogService | 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.HOME_SCREEN | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_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.navigation.StudeezDestinations.SUBJECT_SCREEN | ||||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | @ -25,7 +27,7 @@ class NavigationBarViewModel @Inject constructor( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onSessionsClick(open: (String) -> Unit) { |     fun onSessionsClick(open: (String) -> Unit) { | ||||||
|         open(SESSIONS_SCREEN) |         open(FRIENDS_FEED) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onProfileClick(open: (String) -> Unit) { |     fun onProfileClick(open: (String) -> Unit) { | ||||||
|  | @ -33,13 +35,11 @@ class NavigationBarViewModel @Inject constructor( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onAddTaskClick(open: (String) -> Unit) { |     fun onAddTaskClick(open: (String) -> Unit) { | ||||||
|         // TODO open(CREATE_TASK_SCREEN) |         open(SELECT_SUBJECT) | ||||||
|         SnackbarManager.showMessage(AppText.create_task_not_possible_yet) // TODO Remove |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onAddFriendClick(open: (String) -> Unit) { |     fun onAddFriendClick(open: (String) -> Unit) { | ||||||
|         // TODO open(SEARCH_FRIENDS_SCREEN) |         open(SEARCH_FRIENDS_SCREEN) | ||||||
|         SnackbarManager.showMessage(AppText.add_friend_not_possible_yet) // TODO Remove |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onAddSessionClick(open: (String) -> Unit) { |     fun onAddSessionClick(open: (String) -> Unit) { | ||||||
|  |  | ||||||
|  | @ -1,20 +1,17 @@ | ||||||
| package be.ugent.sel.studeez.common.composable.tasks | package be.ugent.sel.studeez.common.composable.tasks | ||||||
| 
 | 
 | ||||||
| import androidx.compose.foundation.background | import androidx.compose.foundation.background | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.* | ||||||
| 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.shape.CircleShape | import androidx.compose.foundation.shape.CircleShape | ||||||
| import androidx.compose.material.Card | import androidx.compose.material.Card | ||||||
| import androidx.compose.material.Icon | import androidx.compose.material.Icon | ||||||
|  | import androidx.compose.material.MaterialTheme | ||||||
| import androidx.compose.material.Text | import androidx.compose.material.Text | ||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.List | import androidx.compose.material.icons.filled.List | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.collectAsState | ||||||
|  | import androidx.compose.runtime.getValue | ||||||
| import androidx.compose.ui.Alignment | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.draw.clip | 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.text.style.TextOverflow | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | 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.common.composable.StealthButton | ||||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | import be.ugent.sel.studeez.data.local.models.task.Subject | ||||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | 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 | @Composable | ||||||
| fun SubjectEntry( | fun SubjectEntry( | ||||||
|     subject: Subject, |     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( |     Card( | ||||||
|         modifier = Modifier |         modifier = Modifier | ||||||
|             .fillMaxWidth() |             .fillMaxWidth() | ||||||
|  | @ -61,16 +66,17 @@ fun SubjectEntry( | ||||||
|                 ) { |                 ) { | ||||||
|                     Text( |                     Text( | ||||||
|                         text = subject.name, |                         text = subject.name, | ||||||
|                         fontWeight = FontWeight.Bold, |  | ||||||
|                         overflow = TextOverflow.Ellipsis, |                         overflow = TextOverflow.Ellipsis, | ||||||
|                         maxLines = 1, |                         maxLines = 1, | ||||||
|  |                         fontWeight = FontWeight.Medium | ||||||
|                     ) |                     ) | ||||||
|                     Row( |                     Row( | ||||||
|                         horizontalArrangement = Arrangement.spacedBy(10.dp), |                         horizontalArrangement = Arrangement.spacedBy(10.dp), | ||||||
|                         verticalAlignment = Alignment.CenterVertically, |                         verticalAlignment = Alignment.CenterVertically | ||||||
|                     ) { |                     ) { | ||||||
|                         Text( |                         Text( | ||||||
|                             text = HoursMinutesSeconds(subject.time).toString(), |                             text = HoursMinutesSeconds(studytime).toString(), | ||||||
|  |                             color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f) | ||||||
|                         ) |                         ) | ||||||
|                         Row( |                         Row( | ||||||
|                             verticalAlignment = Alignment.CenterVertically, |                             verticalAlignment = Alignment.CenterVertically, | ||||||
|  | @ -78,21 +84,18 @@ fun SubjectEntry( | ||||||
|                         ) { |                         ) { | ||||||
|                             Icon( |                             Icon( | ||||||
|                                 imageVector = Icons.Default.List, |                                 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( |             selectButton(this) | ||||||
|                 text = AppText.view_tasks, |  | ||||||
|                 modifier = Modifier |  | ||||||
|                     .padding(start = 10.dp, end = 5.dp) |  | ||||||
|                     .weight(1f) |  | ||||||
|             ) { |  | ||||||
|                 onViewSubject() |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -104,9 +107,17 @@ fun SubjectEntryPreview() { | ||||||
|         subject = Subject( |         subject = Subject( | ||||||
|             name = "Test Subject", |             name = "Test Subject", | ||||||
|             argb_color = 0xFFFFD200, |             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 | @Preview | ||||||
|  | @ -116,7 +127,9 @@ fun OverflowSubjectEntryPreview() { | ||||||
|         subject = Subject( |         subject = Subject( | ||||||
|             name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt", |             name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt", | ||||||
|             argb_color = 0xFFFFD200, |             argb_color = 0xFFFFD200, | ||||||
|             time = 60 |  | ||||||
|         ), |         ), | ||||||
|  |         getTaskCount = { flowOf() }, | ||||||
|  |         getCompletedTaskCount = { flowOf() }, | ||||||
|  |         getStudyTime = { flowOf() }, | ||||||
|     ) {} |     ) {} | ||||||
| } | } | ||||||
|  | @ -1,17 +1,7 @@ | ||||||
| package be.ugent.sel.studeez.common.composable.tasks | package be.ugent.sel.studeez.common.composable.tasks | ||||||
| 
 | 
 | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.* | ||||||
| import androidx.compose.foundation.layout.Box | import androidx.compose.material.* | ||||||
| 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.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Delete | import androidx.compose.material.icons.filled.Delete | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
|  | @ -31,7 +21,8 @@ import be.ugent.sel.studeez.resources | ||||||
| fun TaskEntry( | fun TaskEntry( | ||||||
|     task: Task, |     task: Task, | ||||||
|     onCheckTask: (Boolean) -> Unit, |     onCheckTask: (Boolean) -> Unit, | ||||||
|     onDeleteTask: () -> Unit, |     onArchiveTask: () -> Unit, | ||||||
|  |     onStartTask: () -> Unit | ||||||
| ) { | ) { | ||||||
|     Card( |     Card( | ||||||
|         modifier = Modifier |         modifier = Modifier | ||||||
|  | @ -80,7 +71,7 @@ fun TaskEntry( | ||||||
|             Box(modifier = Modifier.weight(7f)) { |             Box(modifier = Modifier.weight(7f)) { | ||||||
|                 if (task.completed) { |                 if (task.completed) { | ||||||
|                     IconButton( |                     IconButton( | ||||||
|                         onClick = onDeleteTask, |                         onClick = onArchiveTask, | ||||||
|                         modifier = Modifier |                         modifier = Modifier | ||||||
|                             .padding(start = 20.dp) |                             .padding(start = 20.dp) | ||||||
|                     ) { |                     ) { | ||||||
|  | @ -95,6 +86,7 @@ fun TaskEntry( | ||||||
|                         modifier = Modifier |                         modifier = Modifier | ||||||
|                             .padding(end = 5.dp), |                             .padding(end = 5.dp), | ||||||
|                     ) { |                     ) { | ||||||
|  |                         onStartTask() | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -110,7 +102,7 @@ fun TaskEntryPreview() { | ||||||
|             name = "Test Task", |             name = "Test Task", | ||||||
|             completed = false, |             completed = false, | ||||||
|         ), |         ), | ||||||
|         {}, {}, |         {}, {}, {} | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -122,7 +114,7 @@ fun CompletedTaskEntryPreview() { | ||||||
|             name = "Test Task", |             name = "Test Task", | ||||||
|             completed = true, |             completed = true, | ||||||
|         ), |         ), | ||||||
|         {}, {}, |         {}, {}, {}, | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -134,6 +126,6 @@ fun OverflowTaskEntryPreview() { | ||||||
|             name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", |             name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", | ||||||
|             completed = false, |             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( | data class SessionReport( | ||||||
|     @DocumentId val id: String = "", |     @DocumentId val id: String = "", | ||||||
|     val studyTime: Int = 0, |     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 | 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( | data class Subject( | ||||||
|     @DocumentId val id: String = "", |     @DocumentId val id: String = "", | ||||||
|     val name: String = "", |     val name: String = "", | ||||||
|     val time: Int = 0, |  | ||||||
|     val argb_color: Long = 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( | data class Task( | ||||||
|     @DocumentId val id: String = "", |     @DocumentId val id: String = "", | ||||||
|     val name: String = "", |     val name: String = "", | ||||||
|     val completed: Boolean = false, |     var completed: Boolean = false, | ||||||
|     val time: Int = 0, |     val time: Int = 0, | ||||||
|     val subjectId: String = "", |     val subjectId: String = "", | ||||||
|  |     var archived: Boolean = false, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| object TaskDocument { | object TaskDocument { | ||||||
|  | @ -16,4 +17,5 @@ object TaskDocument { | ||||||
|     const val completed = "completed" |     const val completed = "completed" | ||||||
|     const val time = "time" |     const val time = "time" | ||||||
|     const val subjectId = "subjectId" |     const val subjectId = "subjectId" | ||||||
|  |     const val archived = "archived" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,17 +2,17 @@ package be.ugent.sel.studeez.data.local.models.timer_functional | ||||||
| 
 | 
 | ||||||
| class FunctionalPomodoroTimer( | class FunctionalPomodoroTimer( | ||||||
|     private var studyTime: Int, |     private var studyTime: Int, | ||||||
|     private var breakTime: Int, repeats: Int |     private var breakTime: Int, | ||||||
|  |     val repeats: Int | ||||||
| ) : FunctionalTimer(studyTime) { | ) : FunctionalTimer(studyTime) { | ||||||
| 
 | 
 | ||||||
|     var breaksRemaining = repeats |     var breaksRemaining = repeats - 1 | ||||||
|     var isInBreak = false |     var isInBreak = false | ||||||
| 
 | 
 | ||||||
|     override fun tick() { |     override fun tick() { | ||||||
|         if (hasEnded()) { |         if (hasEnded()) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         if (hasCurrentCountdownEnded()) { |         if (hasCurrentCountdownEnded()) { | ||||||
|             if (isInBreak) { |             if (isInBreak) { | ||||||
|                 breaksRemaining-- |                 breaksRemaining-- | ||||||
|  |  | ||||||
|  | @ -17,10 +17,12 @@ abstract class FunctionalTimer(initialValue: Int) { | ||||||
| 
 | 
 | ||||||
|     abstract fun hasCurrentCountdownEnded(): Boolean |     abstract fun hasCurrentCountdownEnded(): Boolean | ||||||
| 
 | 
 | ||||||
|     fun getSessionReport(): SessionReport { |     fun getSessionReport(subjectId: String, taskId: String): SessionReport { | ||||||
|         return SessionReport( |         return SessionReport( | ||||||
|             studyTime = totalStudyTime, |             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 |     @Binds | ||||||
|     abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO |     abstract fun provideUserDAO(impl: FirebaseUserDAO): UserDAO | ||||||
| 
 | 
 | ||||||
|  |     @Binds | ||||||
|  |     abstract fun provideFriendshipDAO(impl: FirebaseFriendshipDAO): FriendshipDAO | ||||||
|  | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO |     abstract fun provideTimerDAO(impl: FirebaseTimerDAO): TimerDAO | ||||||
| 
 | 
 | ||||||
|  | @ -26,11 +29,14 @@ abstract class DatabaseModule { | ||||||
|     abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService |     abstract fun provideConfigurationService(impl: FirebaseConfigurationService): ConfigurationService | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideSessionDAO(impl: FireBaseSessionDAO): SessionDAO |     abstract fun provideSessionDAO(impl: FirebaseSessionDAO): SessionDAO | ||||||
| 
 | 
 | ||||||
|     @Binds |     @Binds | ||||||
|     abstract fun provideSubjectDAO(impl: FireBaseSubjectDAO): SubjectDAO |     abstract fun provideSubjectDAO(impl: FirebaseSubjectDAO): SubjectDAO | ||||||
| 
 | 
 | ||||||
|     @Binds |     @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 | 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.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.local.models.timer_info.TimerInfo | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| 
 | 
 | ||||||
| interface SessionDAO { | interface SessionDAO { | ||||||
| 
 | 
 | ||||||
|     fun getSessions(): Flow<List<SessionReport>> |     fun getSessions(): Flow<List<SessionReport>> | ||||||
|  |     suspend fun getSessionsOfUser(userId: String): List<SessionReport> | ||||||
| 
 | 
 | ||||||
|     fun saveSession(newSessionReport: SessionReport) |     fun saveSession(newSessionReport: SessionReport) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,4 +12,13 @@ interface SubjectDAO { | ||||||
|     fun deleteSubject(oldSubject: Subject) |     fun deleteSubject(oldSubject: Subject) | ||||||
| 
 | 
 | ||||||
|     fun updateSubject(newSubject: 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 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 | package be.ugent.sel.studeez.domain | ||||||
| 
 | 
 | ||||||
|  | import be.ugent.sel.studeez.data.local.models.User | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | 
 | ||||||
| interface UserDAO { | interface UserDAO { | ||||||
| 
 | 
 | ||||||
|     suspend fun getUsername(): String? |     fun getCurrentUserId(): String | ||||||
|     suspend fun save(newUsername: 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. |      * 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 | package be.ugent.sel.studeez.domain.implementation | ||||||
| 
 | 
 | ||||||
| object FireBaseCollections { | object FirebaseCollections { | ||||||
|     const val SESSION_COLLECTION = "sessions" |     const val SESSION_COLLECTION = "sessions" | ||||||
|     const val USER_COLLECTION = "users" |     const val USER_COLLECTION = "users" | ||||||
|  |     const val FRIENDS_COLLECTION = "friends" | ||||||
|     const val TIMER_COLLECTION = "timers" |     const val TIMER_COLLECTION = "timers" | ||||||
|     const val SUBJECT_COLLECTION = "subjects" |     const val SUBJECT_COLLECTION = "subjects" | ||||||
|     const val TASK_COLLECTION = "tasks" |     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 = |     private fun currentUserTimersCollection(): CollectionReference = | ||||||
|         firestore.collection(FireBaseCollections.USER_COLLECTION) |         firestore.collection(FirebaseCollections.USER_COLLECTION) | ||||||
|             .document(auth.currentUserId) |             .document(auth.currentUserId) | ||||||
|             .collection(FireBaseCollections.TIMER_COLLECTION) |             .collection(FirebaseCollections.TIMER_COLLECTION) | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -2,34 +2,91 @@ package be.ugent.sel.studeez.domain.implementation | ||||||
| 
 | 
 | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | 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.AccountDAO | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | 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.DocumentReference | ||||||
| import com.google.firebase.firestore.FirebaseFirestore | 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 kotlinx.coroutines.tasks.await | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| class FirebaseUserDAO @Inject constructor( | class FirebaseUserDAO @Inject constructor( | ||||||
|     private val firestore: FirebaseFirestore, |     private val firestore: FirebaseFirestore, | ||||||
|     private val auth: AccountDAO |     private val auth: AccountDAO | ||||||
|     ) : UserDAO { | ) : UserDAO { | ||||||
| 
 | 
 | ||||||
|     override suspend fun getUsername(): String? { |     override fun getCurrentUserId(): String { | ||||||
|         return currentUserDocument().get().await().getString("username") |         return auth.currentUserId | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override suspend fun save(newUsername: String) { |  | ||||||
|         currentUserDocument().set(mapOf("username" to newUsername)) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun currentUserDocument(): DocumentReference = |     private fun currentUserDocument(): DocumentReference = | ||||||
|         firestore.collection(USER_COLLECTION).document(auth.currentUserId) |         firestore | ||||||
|  |             .collection(USER_COLLECTION) | ||||||
|  |             .document(auth.currentUserId) | ||||||
| 
 | 
 | ||||||
|     companion object { |     override fun getAllUsers(): Flow<List<User>> { | ||||||
|         private const val USER_COLLECTION = "users" |         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() |         currentUserDocument().delete() | ||||||
|             .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } |             .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } | ||||||
|             .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } |             .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ object StudeezDestinations { | ||||||
|     // NavBar |     // NavBar | ||||||
|     const val HOME_SCREEN = "home" |     const val HOME_SCREEN = "home" | ||||||
|     const val SUBJECT_SCREEN = "subjects" |     const val SUBJECT_SCREEN = "subjects" | ||||||
|     const val SESSIONS_SCREEN = "sessions" |     const val FRIENDS_FEED = "friends_feed" | ||||||
|     const val PROFILE_SCREEN = "profile" |     const val PROFILE_SCREEN = "profile" | ||||||
| 
 | 
 | ||||||
|     // Drawer |     // Drawer | ||||||
|  | @ -27,10 +27,13 @@ object StudeezDestinations { | ||||||
|     const val EDIT_SUBJECT_FORM = "edit_subject" |     const val EDIT_SUBJECT_FORM = "edit_subject" | ||||||
|     const val TASKS_SCREEN = "tasks" |     const val TASKS_SCREEN = "tasks" | ||||||
|     const val ADD_TASK_FORM = "add_task" |     const val ADD_TASK_FORM = "add_task" | ||||||
|  |     const val SELECT_SUBJECT = "select_subject" | ||||||
|     const val EDIT_TASK_FORM = "edit_task" |     const val EDIT_TASK_FORM = "edit_task" | ||||||
| 
 | 
 | ||||||
|     // Friends flow |     // Friends flow | ||||||
|  |     const val FRIENDS_OVERVIEW_SCREEN = "friends_overview" | ||||||
|     const val SEARCH_FRIENDS_SCREEN = "search_friends" |     const val SEARCH_FRIENDS_SCREEN = "search_friends" | ||||||
|  |     const val PUBLIC_PROFILE_SCREEN = "public_profile" | ||||||
| 
 | 
 | ||||||
|     // Create & edit screens |     // Create & edit screens | ||||||
|     const val CREATE_TASK_SCREEN = "create_task" |     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.NavigationBarActions | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | 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.home.HomeRoute | ||||||
| import be.ugent.sel.studeez.screens.log_in.LoginRoute | 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.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.SessionRoute | ||||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute | 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.settings.SettingsRoute | ||||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpRoute | import be.ugent.sel.studeez.screens.sign_up.SignUpRoute | ||||||
| import be.ugent.sel.studeez.screens.splash.SplashRoute | 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.TaskRoute | ||||||
| import be.ugent.sel.studeez.screens.tasks.forms.SubjectAddRoute | import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute | ||||||
| import be.ugent.sel.studeez.screens.tasks.forms.SubjectEditRoute | import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute | ||||||
| import be.ugent.sel.studeez.screens.tasks.forms.TaskAddRoute |  | ||||||
| import be.ugent.sel.studeez.screens.tasks.forms.TaskEditRoute |  | ||||||
| import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute | 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.TimerEditRoute | ||||||
| import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen | 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 open: (String) -> Unit = { appState.navigate(it) } | ||||||
|     val openAndPopUp: (String, String) -> Unit = |     val openAndPopUp: (String, String) -> Unit = | ||||||
|         { route, popUp -> appState.navigateAndPopUp(route, popUp) } |         { route, popUp -> appState.navigateAndPopUp(route, popUp) } | ||||||
|  |     val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) } | ||||||
| 
 | 
 | ||||||
|     val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) |     val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) | ||||||
|     val navigationBarActions: NavigationBarActions = |     val navigationBarActions: NavigationBarActions = | ||||||
|  | @ -64,10 +69,11 @@ fun StudeezNavGraph( | ||||||
|         // NavBar |         // NavBar | ||||||
|         composable(StudeezDestinations.HOME_SCREEN) { |         composable(StudeezDestinations.HOME_SCREEN) { | ||||||
|             HomeRoute( |             HomeRoute( | ||||||
|                 open, |                 open = open, | ||||||
|                 viewModel = hiltViewModel(), |  | ||||||
|                 drawerActions = drawerActions, |                 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) { |         composable(StudeezDestinations.ADD_SUBJECT_FORM) { | ||||||
|             SubjectAddRoute( |             SubjectCreateRoute( | ||||||
|                 goBack = goBack, |                 goBack = goBack, | ||||||
|                 openAndPopUp = openAndPopUp, |                 openAndPopUp = openAndPopUp, | ||||||
|                 viewModel = hiltViewModel(), |                 viewModel = hiltViewModel(), | ||||||
|  | @ -98,14 +112,14 @@ fun StudeezNavGraph( | ||||||
| 
 | 
 | ||||||
|         composable(StudeezDestinations.TASKS_SCREEN) { |         composable(StudeezDestinations.TASKS_SCREEN) { | ||||||
|             TaskRoute( |             TaskRoute( | ||||||
|                 goBack = goBack, |                 goBack = { openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.TASKS_SCREEN) }, | ||||||
|                 open = open, |                 open = open, | ||||||
|                 viewModel = hiltViewModel(), |                 viewModel = hiltViewModel(), | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         composable(StudeezDestinations.ADD_TASK_FORM) { |         composable(StudeezDestinations.ADD_TASK_FORM) { | ||||||
|             TaskAddRoute( |             TaskCreateRoute( | ||||||
|                 goBack = goBack, |                 goBack = goBack, | ||||||
|                 openAndPopUp = openAndPopUp, |                 openAndPopUp = openAndPopUp, | ||||||
|                 viewModel = hiltViewModel(), |                 viewModel = hiltViewModel(), | ||||||
|  | @ -121,10 +135,11 @@ fun StudeezNavGraph( | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         composable(StudeezDestinations.SESSIONS_SCREEN) { |         composable(StudeezDestinations.FRIENDS_FEED) { | ||||||
|             SessionsRoute( |             FriendsFeedRoute( | ||||||
|                 drawerActions = drawerActions, |                 drawerActions = drawerActions, | ||||||
|                 navigationBarActions = navigationBarActions |                 navigationBarActions = navigationBarActions, | ||||||
|  |                 viewModel = hiltViewModel() | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -200,7 +215,7 @@ fun StudeezNavGraph( | ||||||
| 
 | 
 | ||||||
|         composable(StudeezDestinations.SESSION_RECAP) { |         composable(StudeezDestinations.SESSION_RECAP) { | ||||||
|             SessionRecapRoute( |             SessionRecapRoute( | ||||||
|                 openAndPopUp = openAndPopUp, |                 clearAndNavigate = clearAndNavigate, | ||||||
|                 viewModel = hiltViewModel() |                 viewModel = hiltViewModel() | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  | @ -220,8 +235,28 @@ fun StudeezNavGraph( | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Friends flow |         // Friends flow | ||||||
|  |         composable(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN) { | ||||||
|  |             FriendsOveriewRoute( | ||||||
|  |                 open = open, | ||||||
|  |                 popUp = goBack, | ||||||
|  |                 viewModel = hiltViewModel() | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         composable(StudeezDestinations.SEARCH_FRIENDS_SCREEN) { |         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 |         // 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.material.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.runtime.collectAsState | import androidx.compose.runtime.collectAsState | ||||||
| import androidx.compose.runtime.getValue |  | ||||||
| import androidx.compose.ui.Alignment | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.draw.clip | 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.DateText | ||||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | 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.common.composable.navbar.NavigationBarActions | ||||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | 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.data.local.models.timer_functional.HoursMinutesSeconds | ||||||
|  | @ -33,11 +31,10 @@ fun FriendsFeedRoute( | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions |     navigationBarActions: NavigationBarActions | ||||||
| ) { | ) { | ||||||
|     val friendsFeedUiState by viewModel.uiState.collectAsState() |  | ||||||
|     FriendsFeedScreen( |     FriendsFeedScreen( | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         uiState = friendsFeedUiState, |         viewModel = viewModel | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -45,20 +42,22 @@ fun FriendsFeedRoute( | ||||||
| fun FriendsFeedScreen( | fun FriendsFeedScreen( | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
|     uiState: FriendsFeedUiState, |     viewModel: FriendsFeedViewModel | ||||||
| ) { | ) { | ||||||
|     PrimaryScreenTemplate( |     PrimaryScreenTemplate( | ||||||
|         title = resources().getString(AppText.friends_feed), |         title = resources().getString(AppText.friends_feed), | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         navigationBarActions = navigationBarActions |         navigationBarActions = navigationBarActions | ||||||
|     ) { |     ) { | ||||||
|         when (uiState) { | 
 | ||||||
|             FriendsFeedUiState.Loading -> LoadingFeed() |         val friendsSessions = viewModel.getFriendsSessions().collectAsState(initial = emptyList()) | ||||||
|             is FriendsFeedUiState.Succes -> { |              | ||||||
|                 val friendsSessions = uiState.friendSessions | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         LazyColumn() { |         LazyColumn() { | ||||||
|             // Default Timers, cannot be edited |             // Default Timers, cannot be edited | ||||||
|                     items(friendsSessions) { |             items(friendsSessions.value) { | ||||||
|                 val (day, feedEntries) = it |                 val (day, feedEntries) = it | ||||||
|                 DateText(date = day) |                 DateText(date = day) | ||||||
|                 feedEntries.forEach { (name, feedEntry) -> |                 feedEntries.forEach { (name, feedEntry) -> | ||||||
|  | @ -67,8 +66,7 @@ fun FriendsFeedScreen( | ||||||
|                 Spacer(modifier = Modifier.height(10.dp)) |                 Spacer(modifier = Modifier.height(10.dp)) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|             } | 
 | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| package be.ugent.sel.studeez.screens.friends_feed | 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.FeedDAO | ||||||
| import be.ugent.sel.studeez.domain.LogService | import be.ugent.sel.studeez.domain.LogService | ||||||
|  | import be.ugent.sel.studeez.domain.SessionDAO | ||||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
| import kotlinx.coroutines.flow.SharingStarted | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.flow.map | import kotlinx.coroutines.flow.map | ||||||
| import kotlinx.coroutines.flow.stateIn | import kotlinx.coroutines.flow.toList | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
|  | @ -17,13 +18,10 @@ class FriendsFeedViewModel @Inject constructor( | ||||||
|     logService: LogService |     logService: LogService | ||||||
| ) : StudeezViewModel(logService) { | ) : StudeezViewModel(logService) { | ||||||
| 
 | 
 | ||||||
|     val uiState: StateFlow<FriendsFeedUiState> = feedDAO.getFriendsSessions() |     fun getFriendsSessions(): Flow<List<Pair<String, List<Pair<String, FeedEntry>>>>> { | ||||||
|         .map { it.toList() } |         return feedDAO.getFriendsSessions().map { it.toList() } | ||||||
|         .map { FriendsFeedUiState.Succes(it) } |     } | ||||||
|         .stateIn( | 
 | ||||||
|             scope = viewModelScope, | 
 | ||||||
|             initialValue = FriendsFeedUiState.Loading, |  | ||||||
|             started = SharingStarted.Eagerly, |  | ||||||
|         ) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,14 +5,17 @@ import androidx.compose.material.IconButton | ||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Person | import androidx.compose.material.icons.filled.Person | ||||||
| import androidx.compose.runtime.Composable | 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 androidx.compose.ui.tooling.preview.Preview | ||||||
| import be.ugent.sel.studeez.R | 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.PrimaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | 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.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 | import be.ugent.sel.studeez.resources | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
|  | @ -21,35 +24,43 @@ fun HomeRoute( | ||||||
|     viewModel: HomeViewModel, |     viewModel: HomeViewModel, | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
|  |     feedViewModel: FeedViewModel, | ||||||
| ) { | ) { | ||||||
|  |     val feedUiState by feedViewModel.uiState.collectAsState() | ||||||
|     HomeScreen( |     HomeScreen( | ||||||
|         onStartSessionClick = { viewModel.onStartSessionClick(open) }, |         onViewFriendsClick = { viewModel.onViewFriendsClick(open) }, | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|  |         feedUiState = feedUiState, | ||||||
|  |         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, | ||||||
|  |         onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) } | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun HomeScreen( | fun HomeScreen( | ||||||
|     onStartSessionClick: () -> Unit, |     onViewFriendsClick: () -> Unit, | ||||||
|     drawerActions: DrawerActions, |     drawerActions: DrawerActions, | ||||||
|     navigationBarActions: NavigationBarActions |     navigationBarActions: NavigationBarActions, | ||||||
|  |     feedUiState: FeedUiState, | ||||||
|  |     continueTask: (String, String) -> Unit, | ||||||
|  |     onEmptyFeedHelp: () -> Unit, | ||||||
| ) { | ) { | ||||||
|     PrimaryScreenTemplate( |     PrimaryScreenTemplate( | ||||||
|         title = resources().getString(R.string.home), |         title = resources().getString(R.string.home), | ||||||
|         drawerActions = drawerActions, |         drawerActions = drawerActions, | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         // TODO barAction = { FriendsAction() } |         barAction = { FriendsAction(onViewFriendsClick) } | ||||||
|     ) { |     ) { | ||||||
|         BasicButton(R.string.start_session, Modifier.basicButton()) { |         Feed(feedUiState, continueTask, onEmptyFeedHelp) | ||||||
|             onStartSessionClick() |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Composable | @Composable | ||||||
| fun FriendsAction() { | fun FriendsAction( | ||||||
|     IconButton(onClick = { /*TODO*/ }) { |     onClick: () -> Unit | ||||||
|  | ) { | ||||||
|  |     IconButton(onClick = onClick) { | ||||||
|         Icon( |         Icon( | ||||||
|             imageVector = Icons.Default.Person, |             imageVector = Icons.Default.Person, | ||||||
|             contentDescription = resources().getString(R.string.friends) |             contentDescription = resources().getString(R.string.friends) | ||||||
|  | @ -61,8 +72,40 @@ fun FriendsAction() { | ||||||
| @Composable | @Composable | ||||||
| fun HomeScreenPreview() { | fun HomeScreenPreview() { | ||||||
|     HomeScreen( |     HomeScreen( | ||||||
|         onStartSessionClick = {}, |         onViewFriendsClick = {}, | ||||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), |         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 | 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.domain.LogService | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
|  | @ -9,11 +7,11 @@ import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class HomeViewModel @Inject constructor( | class HomeViewModel @Inject constructor( | ||||||
|     private val accountDAO: AccountDAO, |  | ||||||
|     logService: LogService |     logService: LogService | ||||||
| ) : StudeezViewModel(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 | 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.Icon | ||||||
| import androidx.compose.material.IconButton | import androidx.compose.material.IconButton | ||||||
|  | import androidx.compose.material.Text | ||||||
| import androidx.compose.material.icons.Icons | import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.filled.Edit | import androidx.compose.material.icons.filled.Edit | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.* | ||||||
| import androidx.compose.runtime.LaunchedEffect | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.runtime.mutableStateOf | import androidx.compose.ui.text.style.TextAlign | ||||||
| import androidx.compose.runtime.remember |  | ||||||
| import androidx.compose.runtime.setValue |  | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.composable.Headline | import be.ugent.sel.studeez.common.composable.Headline | ||||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | 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.resources | ||||||
|  | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.emptyFlow | ||||||
| import be.ugent.sel.studeez.R.string as AppText | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| data class ProfileActions( | data class ProfileActions( | ||||||
|     val getUsername: suspend CoroutineScope.() -> String?, |     val getUsername: suspend CoroutineScope.() -> String?, | ||||||
|  |     val getBiography: suspend CoroutineScope.() -> String?, | ||||||
|  |     val getAmountOfFriends: () -> Flow<Int>, | ||||||
|     val onEditProfileClick: () -> Unit, |     val onEditProfileClick: () -> Unit, | ||||||
|  |     val onViewFriendsClick: () -> Unit | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| fun getProfileActions( | fun getProfileActions( | ||||||
|     viewModel: ProfileViewModel, |     viewModel: ProfileViewModel, | ||||||
|     open: (String) -> Unit, |     open: (String) -> Unit | ||||||
| ): ProfileActions { | ): ProfileActions { | ||||||
|     return ProfileActions( |     return ProfileActions( | ||||||
|         getUsername = { viewModel.getUsername() }, |         getUsername = { viewModel.getUsername() }, | ||||||
|  |         getBiography = { viewModel.getBiography() }, | ||||||
|  |         getAmountOfFriends = { viewModel.getAmountOfFriends() }, | ||||||
|         onEditProfileClick = { viewModel.onEditProfileClick(open) }, |         onEditProfileClick = { viewModel.onEditProfileClick(open) }, | ||||||
|  |         onViewFriendsClick = { viewModel.onViewFriendsClick(open) } | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -56,8 +69,12 @@ fun ProfileScreen( | ||||||
|     navigationBarActions: NavigationBarActions, |     navigationBarActions: NavigationBarActions, | ||||||
| ) { | ) { | ||||||
|     var username: String? by remember { mutableStateOf("") } |     var username: String? by remember { mutableStateOf("") } | ||||||
|  |     var biography: String? by remember { mutableStateOf("") } | ||||||
|  |     val amountOfFriends = profileActions.getAmountOfFriends().collectAsState(initial = 0) | ||||||
|  | 
 | ||||||
|     LaunchedEffect(key1 = Unit) { |     LaunchedEffect(key1 = Unit) { | ||||||
|         username = profileActions.getUsername(this) |         username = profileActions.getUsername(this) | ||||||
|  |         biography = profileActions.getBiography(this) | ||||||
|     } |     } | ||||||
|     PrimaryScreenTemplate( |     PrimaryScreenTemplate( | ||||||
|         title = resources().getString(AppText.profile), |         title = resources().getString(AppText.profile), | ||||||
|  | @ -65,7 +82,35 @@ fun ProfileScreen( | ||||||
|         navigationBarActions = navigationBarActions, |         navigationBarActions = navigationBarActions, | ||||||
|         barAction = { EditAction(onClick = profileActions.onEditProfileClick) } |         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, |             imageVector = Icons.Default.Edit, | ||||||
|             contentDescription = resources().getString(AppText.edit_profile) |             contentDescription = resources().getString(AppText.edit_profile) | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -86,8 +130,38 @@ fun EditAction( | ||||||
| @Composable | @Composable | ||||||
| fun ProfileScreenPreview() { | fun ProfileScreenPreview() { | ||||||
|     ProfileScreen( |     ProfileScreen( | ||||||
|         profileActions = ProfileActions({ null }, {}), |         profileActions = ProfileActions({ null }, { null }, { emptyFlow() }, {}, {}), | ||||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), |         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) |         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 | 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.LogService | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | import be.ugent.sel.studeez.domain.UserDAO | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | import be.ugent.sel.studeez.screens.StudeezViewModel | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class ProfileViewModel @Inject constructor( | class ProfileViewModel @Inject constructor( | ||||||
|     private val userDAO: UserDAO, |     private val userDAO: UserDAO, | ||||||
|  |     private val friendshipDAO: FriendshipDAO, | ||||||
|     logService: LogService |     logService: LogService | ||||||
| ) : StudeezViewModel(logService) { | ) : StudeezViewModel(logService) { | ||||||
| 
 | 
 | ||||||
|     suspend fun getUsername(): String? { |     suspend fun getUsername(): String { | ||||||
|         return userDAO.getUsername() |         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) { |     fun onEditProfileClick(open: (String) -> Unit) { | ||||||
|         open(StudeezDestinations.EDIT_PROFILE_SCREEN) |         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.Composable | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.runtime.getValue | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.tooling.preview.Preview | 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.BasicTextButton | ||||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||||
| import be.ugent.sel.studeez.common.ext.textButton | import be.ugent.sel.studeez.common.ext.textButton | ||||||
| import be.ugent.sel.studeez.resources | import be.ugent.sel.studeez.resources | ||||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||||
|  | import be.ugent.sel.studeez.R.string as AppText | ||||||
| 
 | 
 | ||||||
| data class EditProfileActions( | data class EditProfileActions( | ||||||
|     val onUserNameChange: (String) -> Unit, |     val onUserNameChange: (String) -> Unit, | ||||||
|  |     val onBiographyChange: (String) -> Unit, | ||||||
|     val onSaveClick: () -> Unit, |     val onSaveClick: () -> Unit, | ||||||
|     val onDeleteClick: () -> Unit |     val onDeleteClick: () -> Unit | ||||||
| ) | ) | ||||||
|  | @ -25,6 +26,7 @@ fun getEditProfileActions( | ||||||
| ): EditProfileActions { | ): EditProfileActions { | ||||||
|     return EditProfileActions( |     return EditProfileActions( | ||||||
|         onUserNameChange = { viewModel.onUsernameChange(it) }, |         onUserNameChange = { viewModel.onUsernameChange(it) }, | ||||||
|  |         onBiographyChange = { viewModel.onBiographyChange(it) }, | ||||||
|         onSaveClick = { viewModel.onSaveClick() }, |         onSaveClick = { viewModel.onSaveClick() }, | ||||||
|         onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, |         onDeleteClick = { viewModel.onDeleteClick(openAndPopUp) }, | ||||||
|     ) |     ) | ||||||
|  | @ -51,36 +53,49 @@ fun EditProfileScreen( | ||||||
|     editProfileActions: EditProfileActions, |     editProfileActions: EditProfileActions, | ||||||
| ) { | ) { | ||||||
|     SecondaryScreenTemplate( |     SecondaryScreenTemplate( | ||||||
|         title = resources().getString(R.string.editing_profile), |         title = resources().getString(AppText.editing_profile), | ||||||
|         popUp = goBack |         popUp = goBack | ||||||
|     ) { |     ) { | ||||||
|         Column { |         LazyColumn { | ||||||
|  |             item { | ||||||
|                 LabelledInputField( |                 LabelledInputField( | ||||||
|                     value = uiState.username, |                     value = uiState.username, | ||||||
|                     onNewValue = editProfileActions.onUserNameChange, |                     onNewValue = editProfileActions.onUserNameChange, | ||||||
|                 label = R.string.username |                     label = AppText.username | ||||||
|                 ) |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|  |                 LabelledInputField( | ||||||
|  |                     value = uiState.biography, | ||||||
|  |                     onNewValue = editProfileActions.onBiographyChange, | ||||||
|  |                     label = AppText.biography | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|                 BasicTextButton( |                 BasicTextButton( | ||||||
|                 text = R.string.save, |                     text = AppText.save, | ||||||
|                     Modifier.textButton(), |                     Modifier.textButton(), | ||||||
|                     action = { |                     action = { | ||||||
|                         editProfileActions.onSaveClick() |                         editProfileActions.onSaveClick() | ||||||
|                         goBack() |                         goBack() | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|  |             } | ||||||
|  |             item { | ||||||
|                  BasicTextButton( |                  BasicTextButton( | ||||||
|                 text = R.string.delete_profile, |                     text = AppText.delete_profile, | ||||||
|                     Modifier.textButton(), |                     Modifier.textButton(), | ||||||
|                     action = editProfileActions.onDeleteClick |                     action = editProfileActions.onDeleteClick | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Preview | @Preview | ||||||
| @Composable | @Composable | ||||||
| fun EditProfileScreenComposable() { | fun EditProfileScreenComposable() { | ||||||
|     StudeezTheme { |     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 androidx.compose.runtime.mutableStateOf | ||||||
| import be.ugent.sel.studeez.R | import be.ugent.sel.studeez.R | ||||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | 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.AccountDAO | ||||||
| import be.ugent.sel.studeez.domain.LogService | import be.ugent.sel.studeez.domain.LogService | ||||||
| import be.ugent.sel.studeez.domain.UserDAO | import be.ugent.sel.studeez.domain.UserDAO | ||||||
|  | @ -23,7 +24,11 @@ class ProfileEditViewModel @Inject constructor( | ||||||
| 
 | 
 | ||||||
|     init { |     init { | ||||||
|         launchCatching { |         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) |         uiState.value = uiState.value.copy(username = newValue) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun onBiographyChange(newValue: String) { | ||||||
|  |         uiState.value = uiState.value.copy(biography = newValue) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fun onSaveClick() { |     fun onSaveClick() { | ||||||
|         launchCatching { |         launchCatching { | ||||||
|             userDAO.save(uiState.value.username) |             userDAO.saveLoggedInUser( | ||||||
|  |                 newUsername = uiState.value.username, | ||||||
|  |                 newBiography = uiState.value.biography | ||||||
|  |             ) | ||||||
|             SnackbarManager.showMessage(R.string.success) |             SnackbarManager.showMessage(R.string.success) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { |     fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { | ||||||
|         launchCatching { |         launchCatching { | ||||||
|             userDAO.deleteUserReferences() // Delete references |             userDAO.deleteLoggedInUserReferences() // Delete references | ||||||
|             accountDAO.deleteAccount() // Delete authentication |             accountDAO.deleteAccount() // Delete authentication | ||||||
|         } |         } | ||||||
|         openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) |         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 var viewModel: SessionViewModel? = null | ||||||
|     private lateinit var mediaPlayer: MediaPlayer |     private lateinit var mediaPlayer: MediaPlayer | ||||||
| 
 | 
 | ||||||
|     fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { |     fun setParameters(viewModel: SessionViewModel, mediaPlayer: MediaPlayer) { | ||||||
|  |         this.mediaPlayer = mediaPlayer | ||||||
|         this.viewModel = viewModel |         this.viewModel = viewModel | ||||||
|         this.mediaPlayer = mediaplayer |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun updateTimer() { |     suspend fun updateTimer() { | ||||||
|  |  | ||||||
|  | @ -6,28 +6,22 @@ import android.net.Uri | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.ui.platform.LocalContext | import androidx.compose.ui.platform.LocalContext | ||||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | 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.GetSessionScreenComposable | ||||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen |  | ||||||
| 
 | 
 | ||||||
| data class SessionActions( | data class SessionActions( | ||||||
|     val getTimer: () -> FunctionalTimer, |     val getTimer: () -> FunctionalTimer, | ||||||
|     val getTask: () -> String, |     val getTask: () -> String, | ||||||
|     val startMediaPlayer: () -> Unit, |  | ||||||
|     val releaseMediaPlayer: () -> Unit, |  | ||||||
|     val endSession: () -> Unit |     val endSession: () -> Unit | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| private fun getSessionActions( | private fun getSessionActions( | ||||||
|     viewModel: SessionViewModel, |     viewModel: SessionViewModel, | ||||||
|     openAndPopUp: (String, String) -> Unit, |     openAndPopUp: (String, String) -> Unit, | ||||||
|     mediaplayer: MediaPlayer, |  | ||||||
| ): SessionActions { | ): SessionActions { | ||||||
|     return SessionActions( |     return SessionActions( | ||||||
|         getTimer = viewModel::getTimer, |         getTimer = viewModel::getTimer, | ||||||
|         getTask = viewModel::getTask, |         getTask = viewModel::getTask, | ||||||
|         endSession = { viewModel.endSession(openAndPopUp) }, |         endSession = { viewModel.endSession(openAndPopUp) }, | ||||||
|         startMediaPlayer = mediaplayer::start, |  | ||||||
|         releaseMediaPlayer = mediaplayer::release, |  | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -37,20 +31,15 @@ fun SessionRoute( | ||||||
|     openAndPopUp: (String, String) -> Unit, |     openAndPopUp: (String, String) -> Unit, | ||||||
|     viewModel: SessionViewModel, |     viewModel: SessionViewModel, | ||||||
| ) { | ) { | ||||||
|     val context = LocalContext.current |  | ||||||
|     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) |     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||||
|     val mediaplayer = MediaPlayer.create(context, uri) |     val mediaPlayer = MediaPlayer.create(LocalContext.current, uri) | ||||||
|     mediaplayer.isLooping = false |     mediaPlayer.isLooping = false | ||||||
| 
 | 
 | ||||||
|     InvisibleSessionManager.setParameters( |     InvisibleSessionManager.setParameters(viewModel = viewModel, mediaPlayer = mediaPlayer) | ||||||
|         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( |     sessionScreen() | ||||||
|         open = open, |  | ||||||
|         sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer) |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| package be.ugent.sel.studeez.screens.session | package be.ugent.sel.studeez.screens.session | ||||||
| 
 | 
 | ||||||
| import be.ugent.sel.studeez.data.SelectedTimerState | import be.ugent.sel.studeez.data.SelectedSessionReport | ||||||
| import be.ugent.sel.studeez.data.SessionReportState | 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.data.local.models.timer_functional.FunctionalTimer | ||||||
| import be.ugent.sel.studeez.domain.LogService | import be.ugent.sel.studeez.domain.LogService | ||||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||||
|  | @ -11,23 +12,21 @@ import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class SessionViewModel @Inject constructor( | class SessionViewModel @Inject constructor( | ||||||
|     private val selectedTimerState: SelectedTimerState, |     private val selectedTimer: SelectedTimer, | ||||||
|     private val sessionReportState: SessionReportState, |     private val sessionReport: SelectedSessionReport, | ||||||
|  |     private val selectedTask: SelectedTask, | ||||||
|     logService: LogService |     logService: LogService | ||||||
| ) : StudeezViewModel(logService) { | ) : StudeezViewModel(logService) { | ||||||
| 
 |     fun getTimer(): FunctionalTimer { | ||||||
|     private val task : String = "No task selected" // placeholder for tasks implementation |         return selectedTimer() | ||||||
| 
 |  | ||||||
|     fun getTimer() : FunctionalTimer { |  | ||||||
|         return selectedTimerState.selectedTimer!! |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun getTask(): String { |     fun getTask(): String { | ||||||
|         return task |         return selectedTask().name | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun endSession(openAndPopUp: (String, String) -> Unit) { |     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) |         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