merge with dev
This commit is contained in:
		
						commit
						51c8fb122d
					
				
					 40 changed files with 890 additions and 444 deletions
				
			
		
							
								
								
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							|  | @ -42,5 +42,6 @@ | |||
|       <option name="processLiterals" value="true" /> | ||||
|       <option name="processComments" value="true" /> | ||||
|     </inspection_tool> | ||||
|     <inspection_tool class="TestFunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> | ||||
|   </profile> | ||||
| </component> | ||||
|  | @ -66,6 +66,7 @@ dependencies { | |||
|     implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" | ||||
|     implementation 'androidx.compose.material:material:1.2.0' | ||||
| 
 | ||||
| 
 | ||||
|     debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" | ||||
| 
 | ||||
|     // ViewModel | ||||
|  | @ -97,6 +98,9 @@ dependencies { | |||
|     testImplementation 'junit:junit:4.13.2' | ||||
|     androidTestImplementation 'androidx.test.ext:junit:1.1.5' | ||||
| 
 | ||||
|     // Coroutine testing | ||||
|     testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' | ||||
| 
 | ||||
|     // Mocking | ||||
|     testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0' | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import androidx.compose.material.Surface | |||
| import androidx.compose.material.rememberScaffoldState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.ReadOnlyComposable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.Modifier | ||||
|  | @ -21,9 +22,14 @@ import androidx.hilt.navigation.compose.hiltViewModel | |||
| import androidx.navigation.NavHostController | ||||
| import androidx.navigation.compose.NavHost | ||||
| import androidx.navigation.compose.composable | ||||
| import androidx.navigation.compose.currentBackStackEntryAsState | ||||
| import androidx.navigation.compose.rememberNavController | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerViewModel | ||||
| import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.home.HomeRoute | ||||
|  | @ -31,6 +37,7 @@ 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.session.SessionRoute | ||||
| import be.ugent.sel.studeez.screens.session_recap.SessionRecapRoute | ||||
| import be.ugent.sel.studeez.screens.sign_up.SignUpRoute | ||||
| import be.ugent.sel.studeez.screens.splash.SplashRoute | ||||
| import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewRoute | ||||
|  | @ -90,43 +97,52 @@ fun StudeezNavGraph( | |||
|     val drawerViewModel: DrawerViewModel = hiltViewModel() | ||||
|     val navBarViewModel: NavigationBarViewModel = hiltViewModel() | ||||
| 
 | ||||
|     val backStackEntry by appState.navController.currentBackStackEntryAsState() | ||||
|     val getCurrentScreen: () -> String? = { backStackEntry?.destination?.route } | ||||
| 
 | ||||
|     val goBack: () -> Unit = { appState.popUp() } | ||||
|     val open: (String) -> Unit = { appState.navigate(it) } | ||||
|     val openAndPopUp: (String, String) -> Unit = | ||||
|         { route, popUp -> appState.navigateAndPopUp(route, popUp) } | ||||
| 
 | ||||
|     val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) | ||||
|     val navigationBarActions: NavigationBarActions = | ||||
|         getNavigationBarActions(navBarViewModel, open, getCurrentScreen) | ||||
| 
 | ||||
|     NavHost( | ||||
|         navController = appState.navController, | ||||
|         startDestination = StudeezDestinations.SPLASH_SCREEN, | ||||
|         modifier = modifier, | ||||
|     ) { | ||||
|         val goBack: () -> Unit = { | ||||
|             appState.popUp() | ||||
|         } | ||||
| 
 | ||||
|         val open: (String) -> Unit = { route -> | ||||
|             appState.navigate(route) | ||||
|         } | ||||
| 
 | ||||
|         val openAndPopUp: (String, String) -> Unit = { route, popUp -> | ||||
|             appState.navigateAndPopUp(route, popUp) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         composable(StudeezDestinations.SPLASH_SCREEN) { | ||||
|             SplashRoute(openAndPopUp, viewModel = hiltViewModel()) | ||||
|             SplashRoute( | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.LOGIN_SCREEN) { | ||||
|             LoginRoute(openAndPopUp, viewModel = hiltViewModel()) | ||||
|             LoginRoute( | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.SIGN_UP_SCREEN) { | ||||
|             SignUpRoute(openAndPopUp, viewModel = hiltViewModel()) | ||||
|             SignUpRoute( | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.HOME_SCREEN) { | ||||
|             HomeRoute( | ||||
|                 open, | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 drawerViewModel = drawerViewModel, | ||||
|                 navBarViewModel = navBarViewModel, | ||||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  | @ -134,21 +150,28 @@ fun StudeezNavGraph( | |||
|         // TODO Sessions screen | ||||
| 
 | ||||
|         composable(StudeezDestinations.PROFILE_SCREEN) { | ||||
|             ProfileRoute(open, openAndPopUp, viewModel = hiltViewModel()) | ||||
|             ProfileRoute( | ||||
|                 open, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.TIMER_OVERVIEW_SCREEN) { | ||||
|             TimerOverviewRoute( | ||||
|                 open, | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 drawerViewModel = drawerViewModel, | ||||
|                 navBarViewModel = navBarViewModel, | ||||
|                 drawerActions = drawerActions, | ||||
|                 open = open | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.SESSION_SCREEN) { | ||||
|             SessionRoute(open, viewModel = hiltViewModel()) | ||||
|             SessionRoute( | ||||
|                 open, | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         // TODO Timers screen | ||||
|  | @ -156,16 +179,25 @@ fun StudeezNavGraph( | |||
| 
 | ||||
|         // Edit screens | ||||
|         composable(StudeezDestinations.EDIT_PROFILE_SCREEN) { | ||||
|             EditProfileRoute(goBack, openAndPopUp, viewModel = hiltViewModel()) | ||||
|             EditProfileRoute( | ||||
|                 goBack, | ||||
|                 openAndPopUp, | ||||
|                 viewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.TIMER_SELECTION_SCREEN) { | ||||
|             TimerSelectionRoute( | ||||
|                 open, | ||||
|                 openAndPopUp, | ||||
|                 goBack, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 drawerViewModel = drawerViewModel, | ||||
|                 navBarViewModel = navBarViewModel, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composable(StudeezDestinations.SESSION_RECAP) { | ||||
|             SessionRecapRoute( | ||||
|                 openAndPopUp = openAndPopUp, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,9 +10,15 @@ import androidx.compose.material.Text | |||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import be.ugent.sel.studeez.StudeezApp | ||||
| import be.ugent.sel.studeez.screens.session.InvisibleSessionManager | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import dagger.hilt.android.AndroidEntryPoint | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.launch | ||||
| 
 | ||||
| var onTimerInvisible: Job? = null | ||||
| 
 | ||||
| @AndroidEntryPoint | ||||
| class MainActivity : ComponentActivity() { | ||||
|  | @ -30,6 +36,18 @@ class MainActivity : ComponentActivity() { | |||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onStop() { | ||||
|         onTimerInvisible = lifecycleScope.launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|         } | ||||
|         super.onStop() | ||||
|     } | ||||
| 
 | ||||
|     override fun onStart() { | ||||
|         onTimerInvisible?.cancel() | ||||
|         super.onStart() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
|  |  | |||
|  | @ -3,15 +3,9 @@ package be.ugent.sel.studeez.common.composable | |||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.BorderStroke | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.ButtonColors | ||||
| import androidx.compose.material.ButtonDefaults | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
|  | @ -21,7 +15,6 @@ import be.ugent.sel.studeez.common.ext.basicButton | |||
| import be.ugent.sel.studeez.common.ext.card | ||||
| 
 | ||||
| @Composable | ||||
| 
 | ||||
| fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { | ||||
|     TextButton( | ||||
|         onClick = action, | ||||
|  | @ -71,10 +64,10 @@ fun StealthButton( | |||
|         onClick = onClick, | ||||
|         modifier = Modifier.card(), | ||||
|         colors = ButtonDefaults.buttonColors( | ||||
|             backgroundColor = Color.Transparent, | ||||
|             contentColor = Color.DarkGray, | ||||
|             backgroundColor = MaterialTheme.colors.surface, | ||||
|             contentColor = MaterialTheme.colors.onSurface | ||||
|         ), | ||||
|         border = BorderStroke(3.dp, Color.DarkGray), | ||||
|         border = BorderStroke(1.dp, MaterialTheme.colors.onSurface) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,64 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Menu | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.common.composable.drawer.Drawer | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.launch | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun DrawerScreenTemplate( | ||||
|     title: String, | ||||
|     drawerActions: DrawerActions, | ||||
|     barAction: @Composable RowScope.() -> Unit = {}, | ||||
|     content: @Composable (PaddingValues) -> Unit | ||||
| ) { | ||||
|     val scaffoldState: ScaffoldState = rememberScaffoldState() | ||||
|     val coroutineScope: CoroutineScope = rememberCoroutineScope() | ||||
| 
 | ||||
|     Scaffold( | ||||
|         scaffoldState = scaffoldState, | ||||
| 
 | ||||
|         topBar = { TopAppBar( | ||||
|             title = { Text(text = title) }, | ||||
|             navigationIcon = { | ||||
|                 IconButton(onClick = { | ||||
|                     coroutineScope.launch { scaffoldState.drawerState.open() } | ||||
|                 }) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Default.Menu, | ||||
|                         contentDescription = resources().getString(AppText.menu) | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|             actions = barAction | ||||
|         )}, | ||||
| 
 | ||||
|         drawerContent = { | ||||
|             Drawer(drawerActions) | ||||
|         } | ||||
|     ) { | ||||
|         content(it) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun DrawerScreenPreview() { | ||||
|     StudeezTheme { DrawerScreenTemplate( | ||||
|         title = "Drawer screen preview", | ||||
|         drawerActions =DrawerActions({}, {}, {}, {}, {}) | ||||
|     ) { | ||||
|         Text(text = "Preview content") | ||||
|     } } | ||||
| } | ||||
|  | @ -2,26 +2,19 @@ package be.ugent.sel.studeez.common.composable | |||
| 
 | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.material.FabPosition | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.IconButton | ||||
| import androidx.compose.material.Scaffold | ||||
| import androidx.compose.material.ScaffoldState | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TopAppBar | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Edit | ||||
| import androidx.compose.material.icons.filled.Menu | ||||
| import androidx.compose.material.rememberScaffoldState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.common.composable.drawer.Drawer | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBar | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.launch | ||||
|  | @ -31,7 +24,7 @@ fun PrimaryScreenTemplate( | |||
|     title: String, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     action: @Composable RowScope.() -> Unit = {}, | ||||
|     barAction: @Composable RowScope.() -> Unit = {}, | ||||
|     content: @Composable (PaddingValues) -> Unit | ||||
| ) { | ||||
|     val scaffoldState: ScaffoldState = rememberScaffoldState() | ||||
|  | @ -53,7 +46,7 @@ fun PrimaryScreenTemplate( | |||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|                 actions = action | ||||
|                 actions = barAction | ||||
|             ) | ||||
|         }, | ||||
| 
 | ||||
|  | @ -77,7 +70,7 @@ fun PrimaryScreenPreview() { | |||
|         PrimaryScreenTemplate( | ||||
|             "Preview screen", | ||||
|             DrawerActions({}, {}, {}, {}, {}), | ||||
|             NavigationBarActions({}, {}, {}, {}), | ||||
|             NavigationBarActions({ false }, {}, {}, {}, {}), | ||||
|             { | ||||
|                 IconButton(onClick = { /*TODO*/ }) { | ||||
|                     Icon( | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.ArrowBack | ||||
|  | @ -10,13 +11,12 @@ import be.ugent.sel.studeez.R | |||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| 
 | ||||
| // TODO Add option for button in top right corner as extra button | ||||
| 
 | ||||
| @Composable | ||||
| // Does not contain floatingActionButton and bottom bar, used in all the other screens | ||||
| fun SecondaryScreenTemplate( | ||||
|     title: String, | ||||
|     popUp: () -> Unit, | ||||
|     barAction: @Composable RowScope.() -> Unit = {}, | ||||
|     content: @Composable (PaddingValues) -> Unit | ||||
| ) { | ||||
|     Scaffold( | ||||
|  | @ -30,7 +30,8 @@ fun SecondaryScreenTemplate( | |||
|                         contentDescription = resources().getString(R.string.go_back) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             }, | ||||
|             actions = barAction | ||||
|         ) }, | ||||
|     ) { paddingValues -> | ||||
|         content(paddingValues) | ||||
|  |  | |||
|  | @ -1,10 +1,6 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| 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.* | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
|  | @ -20,24 +16,39 @@ import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | |||
| @Composable | ||||
| fun TimerEntry( | ||||
|     timerInfo: TimerInfo, | ||||
|     button: @Composable () -> Unit, | ||||
|     rightButton: @Composable () -> Unit = {}, | ||||
|     leftButton: @Composable () -> Unit = {} | ||||
| ) { | ||||
|     Row( | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         horizontalArrangement = Arrangement.SpaceBetween | ||||
|         modifier = Modifier.fillMaxWidth() | ||||
|     ) { | ||||
|         Row( | ||||
|             modifier = Modifier.weight(1f) | ||||
|         ) { | ||||
|             Box(modifier = Modifier.align(alignment = Alignment.CenterVertically)) { | ||||
|                 leftButton() | ||||
|             } | ||||
| 
 | ||||
|             Column( | ||||
|             Modifier.padding(horizontal = 10.dp) | ||||
|                 Modifier.padding( | ||||
|                     horizontal = 20.dp, | ||||
|                     vertical = 11.dp | ||||
|                 ) | ||||
|             ) { | ||||
|                 Text( | ||||
|                 text = timerInfo.name, fontWeight = FontWeight.Bold, fontSize = 20.sp | ||||
|                     text = timerInfo.name, | ||||
|                     fontWeight = FontWeight.Medium, | ||||
|                     fontSize = 20.sp | ||||
|                 ) | ||||
|                 Text( | ||||
|                 text = timerInfo.description, fontWeight = FontWeight.Light, fontSize = 15.sp | ||||
|                     text = timerInfo.description, fontWeight = FontWeight.Light, fontSize = 14.sp | ||||
|                 ) | ||||
|             } | ||||
|         button() | ||||
|         } | ||||
| 
 | ||||
|         Box(modifier = Modifier.align(alignment = Alignment.CenterVertically)) { | ||||
|             rightButton() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,11 +12,14 @@ import androidx.compose.material.icons.outlined.DateRange | |||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.ui.theme.StudeezTheme | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| data class NavigationBarActions( | ||||
|     val isSelectedTab: (String) -> Boolean, | ||||
|     val onHomeClick: () -> Unit, | ||||
|     val onTasksClick: () -> Unit, | ||||
|     val onSessionsClick: () -> Unit, | ||||
|  | @ -26,29 +29,38 @@ data class NavigationBarActions( | |||
| fun getNavigationBarActions( | ||||
|     navigationBarViewModel: NavigationBarViewModel, | ||||
|     open: (String) -> Unit, | ||||
|     getCurrentScreen: () -> String? | ||||
| ): NavigationBarActions { | ||||
|     return NavigationBarActions( | ||||
|         onHomeClick = { navigationBarViewModel.onHomeClick(open) }, | ||||
|         onTasksClick = { navigationBarViewModel.onTasksClick(open) }, | ||||
|         onSessionsClick = { navigationBarViewModel.onSessionsClick(open) }, | ||||
|         onProfileClick = { navigationBarViewModel.onProfileClick(open) }, | ||||
|         isSelectedTab = { screen -> | ||||
|             screen == getCurrentScreen() | ||||
|         }, | ||||
|         onHomeClick = { | ||||
|             navigationBarViewModel.onHomeClick(open) | ||||
|         }, | ||||
|         onTasksClick = { | ||||
|             navigationBarViewModel.onTasksClick(open) | ||||
|         }, | ||||
|         onSessionsClick = { | ||||
|             navigationBarViewModel.onSessionsClick(open) | ||||
|         }, | ||||
|         onProfileClick = { | ||||
|             navigationBarViewModel.onProfileClick(open) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun NavigationBar( | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     navigationBarActions: NavigationBarActions | ||||
| ) { | ||||
|     // TODO Pass functions and new screens. | ||||
|     // TODO Pass which screen is selected. | ||||
|     // TODO Disabled -> HIGH/MEDIUM_EMPHASIS if the page is implemented | ||||
|     BottomNavigation( | ||||
|         elevation = 10.dp | ||||
|     ) { | ||||
|         BottomNavigationItem( | ||||
|             icon = { Icon(imageVector = Icons.Default.List, resources().getString(AppText.home)) }, | ||||
|             label = { Text(text = resources().getString(AppText.home)) }, | ||||
|             selected = false, // TODO | ||||
|             selected = navigationBarActions.isSelectedTab(HOME_SCREEN), | ||||
|             onClick = navigationBarActions.onHomeClick | ||||
|         ) | ||||
| 
 | ||||
|  | @ -59,7 +71,8 @@ fun NavigationBar( | |||
|                 ) | ||||
|             }, | ||||
|             label = { Text(text = resources().getString(AppText.tasks)) }, | ||||
|             selected = false, // TODO | ||||
|             // TODO selected = navigationBarActions.isSelectedTab(TASKS_SCREEN), | ||||
|             selected = false, | ||||
|             onClick = navigationBarActions.onTasksClick | ||||
|         ) | ||||
| 
 | ||||
|  | @ -73,7 +86,8 @@ fun NavigationBar( | |||
|                 ) | ||||
|             }, | ||||
|             label = { Text(text = resources().getString(AppText.sessions)) }, | ||||
|             selected = false, // TODO | ||||
|             // TODO selected = navigationBarActions.isSelectedTab(SESSIONS_SCREEN), | ||||
|             selected = false, | ||||
|             onClick = navigationBarActions.onSessionsClick | ||||
|         ) | ||||
| 
 | ||||
|  | @ -84,7 +98,7 @@ fun NavigationBar( | |||
|                 ) | ||||
|             }, | ||||
|             label = { Text(text = resources().getString(AppText.profile)) }, | ||||
|             selected = false, // TODO | ||||
|             selected = navigationBarActions.isSelectedTab(PROFILE_SCREEN), | ||||
|             onClick = navigationBarActions.onProfileClick | ||||
|         ) | ||||
| 
 | ||||
|  | @ -95,6 +109,8 @@ fun NavigationBar( | |||
| @Composable | ||||
| fun NavigationBarPreview() { | ||||
|     StudeezTheme { | ||||
|         NavigationBar(NavigationBarActions({}, {}, {}, {})) | ||||
|         NavigationBar( | ||||
|             navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| 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 | ||||
| } | ||||
|  | @ -3,19 +3,22 @@ package be.ugent.sel.studeez.data.local.models.timer_functional | |||
| class FunctionalCustomTimer(studyTime: Int) : FunctionalTimer(studyTime) { | ||||
| 
 | ||||
|     override fun tick() { | ||||
|         if (time.time == 0) { | ||||
|             view = StudyState.DONE | ||||
|         } else { | ||||
|         if (!hasEnded()) { | ||||
|             time.minOne() | ||||
|             totalStudyTime++ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun hasEnded(): Boolean { | ||||
|         return view == StudyState.DONE | ||||
|     } | ||||
| 
 | ||||
|     override fun hasCurrentCountdownEnded(): Boolean { | ||||
|         return time.time == 0 | ||||
|     } | ||||
| 
 | ||||
|     override fun hasCurrentCountdownEnded(): Boolean { | ||||
|         return hasEnded() | ||||
|     } | ||||
| 
 | ||||
|     override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T { | ||||
|         return visitor.visitFunctionalCustomTimer(this) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| package be.ugent.sel.studeez.data.local.models.timer_functional | ||||
| 
 | ||||
| class FunctionalEndlessTimer() : FunctionalTimer(0) { | ||||
| class FunctionalEndlessTimer : FunctionalTimer(0) { | ||||
| 
 | ||||
|     override fun hasEnded(): Boolean { | ||||
|         return false | ||||
|  | @ -12,5 +12,10 @@ class FunctionalEndlessTimer() : FunctionalTimer(0) { | |||
| 
 | ||||
|     override fun tick() { | ||||
|         time.plusOne() | ||||
|         totalStudyTime++ | ||||
|     } | ||||
| 
 | ||||
|     override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T { | ||||
|         return visitor.visitFunctionalEndlessTimer(this) | ||||
|     } | ||||
| } | ||||
|  | @ -9,30 +9,39 @@ class FunctionalPomodoroTimer( | |||
|     var isInBreak = false | ||||
| 
 | ||||
|     override fun tick() { | ||||
|         if (time.time == 0 && breaksRemaining == 0) { | ||||
|             view = StudyState.DONE | ||||
|         if (hasEnded()) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (time.time == 0) { | ||||
|         if (hasCurrentCountdownEnded()) { | ||||
|             if (isInBreak) { | ||||
|                 breaksRemaining-- | ||||
|                 view = StudyState.FOCUS_REMAINING | ||||
|                 time.time = studyTime | ||||
|             } else { | ||||
|                 view = StudyState.BREAK | ||||
|                 time.time = breakTime | ||||
|             } | ||||
|             isInBreak = !isInBreak | ||||
|         } | ||||
|         time.minOne() | ||||
| 
 | ||||
|         if (!isInBreak) { | ||||
|             totalStudyTime++ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun hasEnded(): Boolean { | ||||
|         return breaksRemaining == 0 && time.time == 0 | ||||
|         return !hasBreaksRemaining() && hasCurrentCountdownEnded() | ||||
|     } | ||||
| 
 | ||||
|     private fun hasBreaksRemaining(): Boolean { | ||||
|         return breaksRemaining > 0 | ||||
|     } | ||||
| 
 | ||||
|     override fun hasCurrentCountdownEnded(): Boolean { | ||||
|         return time.time == 0 | ||||
|     } | ||||
| 
 | ||||
|     override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T { | ||||
|         return visitor.visitFunctionalBreakTimer(this) | ||||
|     } | ||||
| } | ||||
|  | @ -1,8 +1,11 @@ | |||
| package be.ugent.sel.studeez.data.local.models.timer_functional | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import com.google.firebase.Timestamp | ||||
| 
 | ||||
| abstract class FunctionalTimer(initialValue: Int) { | ||||
|     val time: Time = Time(initialValue) | ||||
|     var view: StudyState = StudyState.FOCUS | ||||
|     var totalStudyTime: Int = 0 | ||||
| 
 | ||||
|     fun getHoursMinutesSeconds(): HoursMinutesSeconds { | ||||
|         return time.getAsHMS() | ||||
|  | @ -14,8 +17,12 @@ abstract class FunctionalTimer(initialValue: Int) { | |||
| 
 | ||||
|     abstract fun hasCurrentCountdownEnded(): Boolean | ||||
| 
 | ||||
|     enum class StudyState { | ||||
|         FOCUS, DONE, BREAK, FOCUS_REMAINING | ||||
|     fun getSessionReport(): SessionReport { | ||||
|         return SessionReport( | ||||
|             studyTime = totalStudyTime, | ||||
|             endTime = Timestamp.now() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     abstract fun <T> accept(visitor: FunctionalTimerVisitor<T>): T | ||||
| } | ||||
|  | @ -0,0 +1,11 @@ | |||
| package be.ugent.sel.studeez.data.local.models.timer_functional | ||||
| 
 | ||||
| interface FunctionalTimerVisitor<T> { | ||||
| 
 | ||||
|     fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): T | ||||
| 
 | ||||
|     fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): T | ||||
| 
 | ||||
|     fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): T | ||||
| 
 | ||||
| } | ||||
|  | @ -9,6 +9,7 @@ object StudeezDestinations { | |||
|     const val TIMER_OVERVIEW_SCREEN = "timer_overview" | ||||
|     const val TIMER_SELECTION_SCREEN = "timer_selection" | ||||
|     const val SESSION_SCREEN = "session" | ||||
|     const val SESSION_RECAP = "session_recap" | ||||
|  //    const val TASKS_SCREEN = "tasks" | ||||
|  //    const val SESSIONS_SCREEN = "sessions" | ||||
|     const val PROFILE_SCREEN = "profile" | ||||
|  |  | |||
|  | @ -11,26 +11,21 @@ import be.ugent.sel.studeez.R | |||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerViewModel | ||||
| import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.resources | ||||
| 
 | ||||
| @Composable | ||||
| fun HomeRoute( | ||||
|     open: (String) -> Unit, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: HomeViewModel, | ||||
|     drawerViewModel: DrawerViewModel, | ||||
|     navBarViewModel: NavigationBarViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
| ) { | ||||
|     HomeScreen( | ||||
|         onStartSessionClick = { viewModel.onStartSessionClick(open) }, | ||||
|         drawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp), | ||||
|         navigationBarActions = getNavigationBarActions(navBarViewModel, open), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -40,12 +35,11 @@ fun HomeScreen( | |||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
| ) { | ||||
| 
 | ||||
|     PrimaryScreenTemplate( | ||||
|         title = resources().getString(R.string.home), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         action = { FriendsAction() } | ||||
|         barAction = { FriendsAction() } | ||||
|     ) { | ||||
|         BasicButton(R.string.start_session, Modifier.basicButton()) { | ||||
|             onStartSessionClick() | ||||
|  | @ -69,6 +63,6 @@ fun HomeScreenPreview() { | |||
|     HomeScreen( | ||||
|         onStartSessionClick = {}, | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({}, {}, {}, {}) | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}) | ||||
|     ) | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import androidx.compose.runtime.Composable | |||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.hilt.navigation.compose.hiltViewModel | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicTextButton | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
|  |  | |||
|  | @ -11,15 +11,12 @@ import androidx.compose.runtime.mutableStateOf | |||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.hilt.navigation.compose.hiltViewModel | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.Headline | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| 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.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.resources | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
|  | @ -41,13 +38,14 @@ fun getProfileActions( | |||
| @Composable | ||||
| fun ProfileRoute( | ||||
|     open: (String) -> Unit, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: ProfileViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
| ) { | ||||
|     ProfileScreen( | ||||
|         profileActions = getProfileActions(viewModel, open), | ||||
|         drawerActions = getDrawerActions(hiltViewModel(), open, openAndPopUp), | ||||
|         navigationBarActions = getNavigationBarActions(hiltViewModel(), open), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -65,7 +63,7 @@ fun ProfileScreen( | |||
|         title = resources().getString(AppText.profile), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         action = { EditAction(onClick = profileActions.onEditProfileClick) } | ||||
|         barAction = { EditAction(onClick = profileActions.onEditProfileClick) } | ||||
|     ) { | ||||
|         Headline(text = (username ?: resources().getString(R.string.no_username))) | ||||
|     } | ||||
|  | @ -90,6 +88,6 @@ fun ProfileScreenPreview() { | |||
|     ProfileScreen( | ||||
|         profileActions = ProfileActions({ null }, {}), | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({}, {}, {}, {}) | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,29 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import kotlinx.coroutines.delay | ||||
| import javax.inject.Singleton | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| @Singleton | ||||
| object InvisibleSessionManager { | ||||
|     private var viewModel: SessionViewModel? = null | ||||
|     private lateinit var mediaPlayer: MediaPlayer | ||||
| 
 | ||||
|     fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { | ||||
|         this.viewModel = viewModel | ||||
|         this.mediaPlayer = mediaplayer | ||||
|     } | ||||
| 
 | ||||
|     suspend fun updateTimer() { | ||||
|         viewModel?.let { | ||||
|             while (!it.getTimer().hasEnded()) { | ||||
|                 delay(1.seconds) | ||||
|                 it.getTimer().tick() | ||||
|                 if (it.getTimer().hasCurrentCountdownEnded()) { | ||||
|                     mediaPlayer.start() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,56 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import android.media.RingtoneManager | ||||
| import android.net.Uri | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen | ||||
| 
 | ||||
| data class SessionActions( | ||||
|     val getTimer: () -> FunctionalTimer, | ||||
|     val getTask: () -> String, | ||||
|     val startMediaPlayer: () -> Unit, | ||||
|     val releaseMediaPlayer: () -> Unit, | ||||
|     val endSession: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| private fun getSessionActions( | ||||
|     viewModel: SessionViewModel, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     mediaplayer: MediaPlayer, | ||||
| ): SessionActions { | ||||
|     return SessionActions( | ||||
|         getTimer = viewModel::getTimer, | ||||
|         getTask = viewModel::getTask, | ||||
|         endSession = { viewModel.endSession(openAndPopUp) }, | ||||
|         startMediaPlayer = mediaplayer::start, | ||||
|         releaseMediaPlayer = mediaplayer::release, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionRoute( | ||||
|     open: (String) -> Unit, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: SessionViewModel, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|     val mediaplayer = MediaPlayer.create(context, uri) | ||||
|     mediaplayer.isLooping = false | ||||
| 
 | ||||
|     InvisibleSessionManager.setParameters( | ||||
|         viewModel = viewModel, | ||||
|         mediaplayer = mediaplayer | ||||
|     ) | ||||
| 
 | ||||
|     val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen(mediaplayer)) | ||||
| 
 | ||||
|     sessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer) | ||||
|     ) | ||||
| } | ||||
|  | @ -1,212 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import android.media.RingtoneManager | ||||
| import android.net.Uri | ||||
| 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.platform.LocalContext | ||||
| 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.R | ||||
| 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.FunctionalTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer.StudyState | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.resources | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| var timerEnd = false | ||||
| 
 | ||||
| data class SessionActions( | ||||
|     val getTimer: () -> FunctionalTimer, | ||||
|     val getTask: () -> String, | ||||
|     val prepareMediaPlayer: () -> Unit, | ||||
|     val releaseMediaPlayer: () -> Unit, | ||||
| ) | ||||
| 
 | ||||
| fun getSessionActions( | ||||
|     viewModel: SessionViewModel, | ||||
|     mediaplayer: MediaPlayer, | ||||
| ): SessionActions { | ||||
|     return SessionActions( | ||||
|         getTimer = viewModel::getTimer, | ||||
|         getTask = viewModel::getTask, | ||||
|         prepareMediaPlayer = mediaplayer::prepareAsync, | ||||
|         releaseMediaPlayer = mediaplayer::release, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionRoute( | ||||
|     open: (String) -> Unit, | ||||
|     viewModel: SessionViewModel, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|     val mediaplayer = MediaPlayer() | ||||
|     mediaplayer.setDataSource(context, uri) | ||||
|     mediaplayer.setOnCompletionListener { | ||||
|         mediaplayer.stop() | ||||
|         if (timerEnd) { | ||||
| //            mediaplayer.release() | ||||
|         } | ||||
|     } | ||||
|     mediaplayer.setOnPreparedListener { | ||||
| //        mediaplayer.start() | ||||
|     } | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = getSessionActions(viewModel, mediaplayer), | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionScreen( | ||||
|     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 | ||||
|                     open(StudeezDestinations.HOME_SCREEN) | ||||
|                     // Vanaf hier ook naar report gaan als "end session" knop word ingedrukt | ||||
|                 }, | ||||
|                 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 | ||||
| private fun Timer( | ||||
|     sessionActions: SessionActions, | ||||
| ) { | ||||
|     var tikker by remember { mutableStateOf(false) } | ||||
|     LaunchedEffect(tikker) { | ||||
|         delay(1.seconds) | ||||
|         sessionActions.getTimer().tick() | ||||
|         tikker = !tikker | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|         sessionActions.getTimer().hasCurrentCountdownEnded() && !sessionActions.getTimer() | ||||
|             .hasEnded() | ||||
|     ) { | ||||
| //        sessionActions.prepareMediaPlayer() | ||||
|     } | ||||
| 
 | ||||
|     if (!timerEnd && sessionActions.getTimer().hasEnded()) { | ||||
| //        sessionActions.prepareMediaPlayer() | ||||
|         timerEnd = | ||||
|             true // Placeholder, vanaf hier moet het report opgestart worden en de sessie afgesloten | ||||
|     } | ||||
| 
 | ||||
|     val hms = sessionActions.getTimer().getHoursMinutesSeconds() | ||||
|     Column { | ||||
|         Text( | ||||
|             text = "${hms.hours} : ${hms.minutes} : ${hms.seconds}", | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(50.dp), | ||||
|             textAlign = TextAlign.Center, | ||||
|             fontWeight = FontWeight.Bold, | ||||
|             fontSize = 40.sp, | ||||
|         ) | ||||
|         val stateString: String = when (sessionActions.getTimer().view) { | ||||
|             StudyState.DONE -> resources().getString(R.string.state_done) | ||||
|             StudyState.FOCUS -> resources().getString(R.string.state_focus) | ||||
|             StudyState.BREAK -> resources().getString(R.string.state_take_a_break) | ||||
|             StudyState.FOCUS_REMAINING -> (sessionActions.getTimer() as FunctionalPomodoroTimer?)?.breaksRemaining?.let { | ||||
|                 resources().getQuantityString(R.plurals.state_focus_remaining, it, it) | ||||
|             }.toString() | ||||
|         } | ||||
| 
 | ||||
|         Text( | ||||
|             text = stateString, | ||||
|             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) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun TimerPreview() { | ||||
|     Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {})) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun SessionPreview() { | ||||
|     SessionScreen( | ||||
|         open = {}, | ||||
|         sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}) | ||||
|     ) | ||||
| } | ||||
|  | @ -1,20 +1,21 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.SessionReportState | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class SessionViewModel @Inject constructor( | ||||
|     private val selectedTimerState: SelectedTimerState, | ||||
|     private val sessionReportState: SessionReportState, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     private val timer: FunctionalTimer = FunctionalPomodoroTimer(15, 5, 3) | ||||
|     private val task : String = "No task selected" // placeholder for tasks implementation | ||||
| 
 | ||||
|     fun getTimer() : FunctionalTimer { | ||||
|  | @ -24,4 +25,9 @@ class SessionViewModel @Inject constructor( | |||
|     fun getTask(): String { | ||||
|         return task | ||||
|     } | ||||
| 
 | ||||
|     fun endSession(openAndPopUp: (String, String) -> Unit) { | ||||
|         sessionReportState.sessionReport = getTimer().getSessionReport() | ||||
|         openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,138 @@ | |||
| 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.* | ||||
| 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.hours} : ${hms.minutes} : ${hms.seconds}", | ||||
|                 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" }, {}, {}, {})) | ||||
| } | ||||
|  | @ -0,0 +1,45 @@ | |||
| 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,35 @@ | |||
| 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,16 @@ | |||
| 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,18 @@ | |||
| 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,67 @@ | |||
| package be.ugent.sel.studeez.screens.session_recap | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material.ButtonDefaults | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.Time | ||||
| 
 | ||||
| data class SessionRecapActions( | ||||
|     val getSessionReport: () -> SessionReport, | ||||
|     val saveSession: () -> Unit, | ||||
|     val discardSession: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| fun getSessionRecapActions( | ||||
|     viewModel: SessionRecapViewModel, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
| ): SessionRecapActions { | ||||
|     return SessionRecapActions( | ||||
|         viewModel::getSessionReport, | ||||
|         {viewModel.saveSession(openAndPopUp)}, | ||||
|         {viewModel.discardSession(openAndPopUp)} | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionRecapRoute( | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
|     viewModel: SessionRecapViewModel, | ||||
| ) { | ||||
|     SessionRecapScreen( | ||||
|         modifier = modifier, | ||||
|         getSessionRecapActions(viewModel, openAndPopUp) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionRecapScreen(modifier: Modifier, sessionRecapActions: SessionRecapActions) { | ||||
|     val sessionReport: SessionReport = sessionRecapActions.getSessionReport() | ||||
|     val studyTime: Int = sessionReport.studyTime | ||||
|     val hms: HoursMinutesSeconds = Time(studyTime).getAsHMS() | ||||
|     Column( | ||||
|         modifier = modifier | ||||
|     ) { | ||||
|         Text(text = "You studied: ${hms.hours} : ${hms.minutes} : ${hms.seconds}") | ||||
| 
 | ||||
|         BasicButton( | ||||
|             R.string.save, Modifier.basicButton() | ||||
|         ) { | ||||
|             sessionRecapActions.saveSession() | ||||
|         } | ||||
|         BasicButton( | ||||
|             R.string.discard, Modifier.basicButton(), | ||||
|             colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) | ||||
|         ) { | ||||
|             sessionRecapActions.discardSession() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,33 @@ | |||
| package be.ugent.sel.studeez.screens.session_recap | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.SessionReportState | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.SessionDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class SessionRecapViewModel @Inject constructor( | ||||
|     sessionReportState: SessionReportState, | ||||
|     private val sessionDAO: SessionDAO, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     private val report: SessionReport = sessionReportState.sessionReport!! | ||||
| 
 | ||||
|     fun getSessionReport(): SessionReport { | ||||
|         return report | ||||
|     } | ||||
| 
 | ||||
|     fun saveSession(open: (String, String) -> Unit) { | ||||
|         sessionDAO.saveSession(getSessionReport()) | ||||
|         open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) | ||||
|     } | ||||
| 
 | ||||
|     fun discardSession(open: (String, String) -> Unit) { | ||||
|         open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,5 @@ | |||
| package be.ugent.sel.studeez.screens.splash | ||||
| 
 | ||||
| import android.window.SplashScreen | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
|  |  | |||
|  | @ -1,25 +1,17 @@ | |||
| package be.ugent.sel.studeez.screens.timer_overview | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.ui.Modifier | ||||
| 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.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.DrawerScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
| import be.ugent.sel.studeez.common.composable.TimerEntry | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerViewModel | ||||
| import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.CustomTimerInfo | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
|  | @ -50,37 +42,28 @@ fun getTimerOverviewActions( | |||
| @Composable | ||||
| fun TimerOverviewRoute( | ||||
|     open: (String) -> Unit, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: TimerOverviewViewModel, | ||||
|     drawerViewModel: DrawerViewModel, | ||||
|     navBarViewModel: NavigationBarViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
| ) { | ||||
|     TimerOverviewScreen( | ||||
|         timerOverviewActions = getTimerOverviewActions(viewModel, open), | ||||
|         drawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp), | ||||
|         navigationBarActions = getNavigationBarActions(navBarViewModel, open), | ||||
|         drawerActions = drawerActions, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerOverviewScreen( | ||||
|     timerOverviewActions: TimerOverviewActions, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     drawerActions: DrawerActions | ||||
| ) { | ||||
| 
 | ||||
|     val timers = timerOverviewActions.getUserTimers().collectAsState(initial = emptyList()) | ||||
| 
 | ||||
|     // TODO moet geen primary screen zijn: geen navbar nodig | ||||
|     PrimaryScreenTemplate( | ||||
|     DrawerScreenTemplate( | ||||
|         title = resources().getString(R.string.timers), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|     ) { | ||||
|         Column { | ||||
|             LazyColumn( | ||||
|                 verticalArrangement = Arrangement.spacedBy(7.dp) | ||||
|         drawerActions = drawerActions | ||||
|     ) { | ||||
|         LazyColumn { | ||||
|             // Default Timers, cannot be edited | ||||
|             items(timerOverviewActions.getDefaultTimers()) { | ||||
|                 TimerEntry(timerInfo = it) {} | ||||
|  | @ -97,13 +80,14 @@ fun TimerOverviewScreen( | |||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             } | ||||
|             item { | ||||
|                 BasicButton(R.string.add_timer, Modifier.basicButton()) { | ||||
|                     timerOverviewActions.open(StudeezDestinations.ADD_TIMER_SCREEN) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
|  | @ -118,7 +102,6 @@ fun TimerOverviewPreview() { | |||
|             {}, | ||||
|             {} | ||||
|         ), | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({}, {}, {}, {}) | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}) | ||||
|     ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,22 +1,14 @@ | |||
| package be.ugent.sel.studeez.screens.timer_selection | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| 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.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
| import be.ugent.sel.studeez.common.composable.TimerEntry | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerViewModel | ||||
| import be.ugent.sel.studeez.common.composable.drawer.getDrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.getNavigationBarActions | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.resources | ||||
| import kotlinx.coroutines.flow.Flow | ||||
|  | @ -40,43 +32,37 @@ fun getTimerSelectionActions( | |||
| @Composable | ||||
| fun TimerSelectionRoute( | ||||
|     open: (String) -> Unit, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerSelectionViewModel, | ||||
|     drawerViewModel: DrawerViewModel, | ||||
|     navBarViewModel: NavigationBarViewModel, | ||||
| ) { | ||||
|     TimerSelectionScreen( | ||||
|         timerSelectionActions = getTimerSelectionActions(viewModel, open), | ||||
|         drawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp), | ||||
|         navigationBarActions = getNavigationBarActions(navBarViewModel, open), | ||||
|         popUp = popUp | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerSelectionScreen( | ||||
|     timerSelectionActions: TimerSelectionActions, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     popUp: () -> Unit | ||||
| ) { | ||||
|     val timers = timerSelectionActions.getAllTimers().collectAsState(initial = emptyList()) | ||||
|     PrimaryScreenTemplate( | ||||
|     SecondaryScreenTemplate( | ||||
|         title = resources().getString(R.string.timers), | ||||
|         drawerActions = drawerActions, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|     ) { | ||||
|         LazyColumn( | ||||
|             verticalArrangement = Arrangement.spacedBy(7.dp), | ||||
|         popUp = popUp | ||||
|     ) { | ||||
|         LazyColumn { | ||||
|             // All timers | ||||
|             items(timers.value) { timerInfo -> | ||||
|                 TimerEntry( | ||||
|                     timerInfo = timerInfo, | ||||
|                 ) { | ||||
|                     leftButton = { | ||||
|                         StealthButton( | ||||
|                             text = R.string.start, | ||||
|                             onClick = { timerSelectionActions.startSession(timerInfo) } | ||||
|                         ) | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -87,7 +73,6 @@ fun TimerSelectionScreen( | |||
| fun TimerSelectionPreview() { | ||||
|     TimerSelectionScreen( | ||||
|         timerSelectionActions = TimerSelectionActions({ flowOf() }, {}), | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({}, {}, {}, {}), | ||||
|         popUp = {} | ||||
|     ) | ||||
| } | ||||
|  | @ -10,6 +10,7 @@ | |||
|         <!-- Actions --> | ||||
|         <string name="confirm">Confirm</string> | ||||
|         <string name="save">Save</string> | ||||
|         <string name="discard">Discard</string> | ||||
|         <string name="cancel">Cancel</string> | ||||
|         <string name="go_back">Go back</string> | ||||
|         <string name="next">Next</string> | ||||
|  | @ -44,6 +45,7 @@ | |||
| 
 | ||||
|     <!-- Sessions --> | ||||
|     <string name="sessions">Sessions</string> | ||||
|     <string name="end_session">End session</string> | ||||
| 
 | ||||
|     <!-- Profile --> | ||||
|     <string name="profile">Profile</string> | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import org.junit.Assert | ||||
| import org.junit.Test | ||||
| 
 | ||||
|  | @ -36,9 +35,6 @@ class FunctionalCustomTimerUnitTest : FunctionalTimerUnitTest() { | |||
|         timer = FunctionalCustomTimer(0) | ||||
|         timer.tick() | ||||
|         Assert.assertTrue(timer.hasEnded()) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.DONE, | ||||
|             timer.view | ||||
|         ) | ||||
|         Assert.assertTrue(timer.hasEnded()) | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,6 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import org.junit.Assert | ||||
| import org.junit.Test | ||||
| 
 | ||||
|  | @ -37,10 +36,6 @@ class FunctionalEndlessTimerUnitTest : FunctionalTimerUnitTest() { | |||
|         for (i in 1..n) { | ||||
|             timer.tick() | ||||
|             Assert.assertFalse(timer.hasEnded()) | ||||
|             Assert.assertEquals( | ||||
|                 FunctionalTimer.StudyState.FOCUS, | ||||
|                 timer.view | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,6 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import org.junit.Assert | ||||
| import org.junit.Test | ||||
| 
 | ||||
|  | @ -29,10 +28,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | |||
|             breaks, | ||||
|             pomodoroTimer.breaksRemaining, | ||||
|         ) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.FOCUS, | ||||
|             pomodoroTimer.view, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  | @ -52,10 +47,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | |||
|         pomodoroTimer = FunctionalPomodoroTimer(0, 0, 0) | ||||
|         pomodoroTimer.tick() | ||||
|         Assert.assertTrue(pomodoroTimer.hasEnded()) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.DONE, | ||||
|             pomodoroTimer.view, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  | @ -65,10 +56,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | |||
|         } | ||||
|         Assert.assertFalse(pomodoroTimer.hasEnded()) | ||||
|         Assert.assertTrue(pomodoroTimer.isInBreak) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.BREAK, | ||||
|             pomodoroTimer.view | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  | @ -77,10 +64,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | |||
|             pomodoroTimer.tick() | ||||
|         } | ||||
|         Assert.assertTrue(pomodoroTimer.isInBreak) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.BREAK, | ||||
|             pomodoroTimer.view | ||||
|         ) | ||||
|         for (i in 0..breakTime) { | ||||
|             pomodoroTimer.tick() | ||||
|         } | ||||
|  | @ -90,9 +73,5 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() { | |||
|             breaksRemaining, | ||||
|             pomodoroTimer.breaksRemaining | ||||
|         ) | ||||
|         Assert.assertEquals( | ||||
|             FunctionalTimer.StudyState.FOCUS_REMAINING, | ||||
|             pomodoroTimer.view | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,100 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.SessionReportState | ||||
| 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.screens.session.InvisibleSessionManager | ||||
| import be.ugent.sel.studeez.screens.session.SessionViewModel | ||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.test.advanceTimeBy | ||||
| import kotlinx.coroutines.test.runTest | ||||
| import org.junit.Assert | ||||
| import org.junit.Test | ||||
| import org.mockito.kotlin.mock | ||||
| 
 | ||||
| @ExperimentalCoroutinesApi | ||||
| class InvisibleSessionManagerTest { | ||||
|     private var timerState: SelectedTimerState = SelectedTimerState() | ||||
|     private lateinit var viewModel: SessionViewModel | ||||
|     private var mediaPlayer: MediaPlayer = mock() | ||||
| 
 | ||||
|     @Test | ||||
|     fun InvisibleEndlessTimerTest() = runTest { | ||||
|         timerState.selectedTimer = FunctionalEndlessTimer() | ||||
|         viewModel = SessionViewModel(timerState, SessionReportState(), mock()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|         } | ||||
| 
 | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 0) | ||||
|         advanceTimeBy(1_000) // Start tikker | ||||
|         advanceTimeBy(10_000_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 10000) | ||||
| 
 | ||||
|         test.cancel() | ||||
|         return@runTest | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun InvisiblePomodoroTimerTest() = runTest { | ||||
|         val studyTime = 10 | ||||
|         val breakTime = 5 | ||||
|         val repeats = 1 | ||||
|         timerState.selectedTimer = FunctionalPomodoroTimer(studyTime, breakTime, repeats) | ||||
|         viewModel = SessionViewModel(timerState, SessionReportState(), mock()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|         } | ||||
| 
 | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 10) | ||||
|         advanceTimeBy(1_000) // start tikker | ||||
| 
 | ||||
|         advanceTimeBy(9_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 1) | ||||
|         // focus, 9 sec, 1 sec nog | ||||
| 
 | ||||
|         advanceTimeBy(2_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 4) | ||||
|         // pauze, 11 sec bezig, 4 seconden nog pauze | ||||
| 
 | ||||
|         advanceTimeBy(5_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 9) | ||||
|         // 2e focus, 16 sec, 9 sec in 2e focus nog | ||||
| 
 | ||||
|         advanceTimeBy(13_000) | ||||
|         Assert.assertTrue(viewModel.getTimer().hasEnded()) | ||||
|         // Done | ||||
| 
 | ||||
|         test.cancel() | ||||
|         return@runTest | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun InvisibleCustomTimerTest() = runTest { | ||||
|         timerState.selectedTimer = FunctionalCustomTimer(5) | ||||
|         viewModel = SessionViewModel(timerState, SessionReportState(), mock()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|         } | ||||
| 
 | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 5) | ||||
|         advanceTimeBy(1_000) // Start tikker | ||||
|         advanceTimeBy(4_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 1) | ||||
|         advanceTimeBy(1_000) | ||||
|         Assert.assertEquals(viewModel.getTimer().time.time, 0) | ||||
| 
 | ||||
|         test.cancel() | ||||
|         return@runTest | ||||
|     } | ||||
| } | ||||
|  | @ -7,6 +7,8 @@ | |||
| # Specifies the JVM arguments used for the daemon process. | ||||
| # The setting is particularly useful for tweaking memory settings. | ||||
| org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 | ||||
| org.gradle.daemon=true | ||||
| org.gradle.parallel=true | ||||
| # When configured, Gradle will run in incubating parallel mode. | ||||
| # This option should only be used with decoupled projects. More details, visit | ||||
| # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects | ||||
|  |  | |||
		Reference in a new issue
	
	 Rune Dyselinck
						Rune Dyselinck