normal test fails
This commit is contained in:
		
						commit
						31e78856c0
					
				
					 48 changed files with 878 additions and 698 deletions
				
			
		
							
								
								
									
										13
									
								
								.idea/androidTestResultsUserPreferences.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								.idea/androidTestResultsUserPreferences.xml
									
										
									
										generated
									
									
									
								
							|  | @ -291,6 +291,19 @@ | |||
|             </AndroidTestResultsTableState> | ||||
|           </value> | ||||
|         </entry> | ||||
|         <entry key="971972923"> | ||||
|           <value> | ||||
|             <AndroidTestResultsTableState> | ||||
|               <option name="preferredColumnWidths"> | ||||
|                 <map> | ||||
|                   <entry key="Duration" value="90" /> | ||||
|                   <entry key="Pixel_XL_API_30" value="120" /> | ||||
|                   <entry key="Tests" value="360" /> | ||||
|                 </map> | ||||
|               </option> | ||||
|             </AndroidTestResultsTableState> | ||||
|           </value> | ||||
|         </entry> | ||||
|         <entry key="1072291594"> | ||||
|           <value> | ||||
|             <AndroidTestResultsTableState> | ||||
|  |  | |||
|  | @ -123,9 +123,6 @@ dependencies { | |||
|     implementation 'com.google.firebase:firebase-firestore-ktx' | ||||
|     implementation 'com.google.firebase:firebase-perf-ktx' | ||||
|     implementation 'com.google.firebase:firebase-config-ktx' | ||||
| 
 | ||||
|     // Colorpicker | ||||
|     implementation 'com.github.skydoves:colorpicker-compose:1.0.2' | ||||
| } | ||||
| 
 | ||||
| // Allow references to generate code | ||||
|  |  | |||
|  | @ -1,119 +0,0 @@ | |||
| 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.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.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.BreakSessionScreen | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.CustomSessionScreen | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.EndlessSessionScreen | ||||
| import org.junit.Assert | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| 
 | ||||
| class SessionScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun customSessionScreenTest() { | ||||
|         var endSession = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             CustomSessionScreen( | ||||
|                 functionalTimer = FunctionalCustomTimer(0), | ||||
|                 mediaplayer = null | ||||
|             ).invoke( | ||||
|                 open = {}, | ||||
|                 sessionActions = SessionActions( | ||||
|                     {FunctionalCustomTimer(0)}, | ||||
|                     { "" }, | ||||
|                     {}, {}, {endSession = true} | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "end session", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
| 
 | ||||
|         Assert.assertTrue(endSession) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun endlessSessionScreenTest() { | ||||
|         var endSession = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             EndlessSessionScreen() | ||||
|                 .invoke( | ||||
|                     open = {}, | ||||
|                     sessionActions = SessionActions( | ||||
|                         {FunctionalEndlessTimer()}, | ||||
|                         { "" }, | ||||
|                         {}, {}, {endSession = true} | ||||
|                     ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "end session", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
| 
 | ||||
|         Assert.assertTrue(endSession) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun breakSessionScreenTest() { | ||||
|         var endSession = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             BreakSessionScreen( | ||||
|                 funPomoDoroTimer = FunctionalPomodoroTimer(0, 0, 0), | ||||
|                 mediaplayer = null | ||||
|             ) | ||||
|             .invoke( | ||||
|                 open = {}, | ||||
|                 sessionActions = SessionActions( | ||||
|                     {FunctionalPomodoroTimer(0, 0, 0)}, | ||||
|                     { "" }, | ||||
|                     {}, {}, {endSession = true} | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 "end session", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
| 
 | ||||
|         Assert.assertTrue(endSession) | ||||
|     } | ||||
| } | ||||
|  | @ -117,12 +117,15 @@ class SubjectScreenTest { | |||
|                 onAddSubject = { add = true }, | ||||
|                 onViewSubject = { view = true }, | ||||
|                 getStudyTime = { flowOf() }, | ||||
|                 getCompletedTaskCount = { flowOf() }, | ||||
|                 getTaskCount = { flowOf() }, | ||||
|                 uiState = SubjectUiState.Succes( | ||||
|                     listOf( | ||||
|                         Subject( | ||||
|                             id = "", | ||||
|                             name = "Test Subject", | ||||
|                             argb_color = 0xFFFFD200, | ||||
|                             taskCount = 5, taskCompletedCount = 2, | ||||
|                             archived = false | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|  |  | |||
|  | @ -1,37 +0,0 @@ | |||
| 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.data.local.models.timer_info.EndlessTimerInfo | ||||
| import be.ugent.sel.studeez.screens.timer_form.form_screens.EndlessTimerFormScreen | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class TimerScreenTest { | ||||
|     @get:Rule | ||||
|     val composeTestRule = createComposeRule() | ||||
| 
 | ||||
|     @Test | ||||
|     fun timerFormScreenTest() { | ||||
|         var save = false | ||||
| 
 | ||||
|         composeTestRule.setContent { | ||||
|             EndlessTimerFormScreen(EndlessTimerInfo("", "")) | ||||
|                 .invoke(onSaveClick = {save = true}) | ||||
|         } | ||||
| 
 | ||||
|         composeTestRule.waitForIdle() | ||||
| 
 | ||||
|         composeTestRule | ||||
|             .onNodeWithText( | ||||
|                 text = "save", | ||||
|                 substring = true, | ||||
|                 ignoreCase = true | ||||
|             ) | ||||
|             .assertExists() | ||||
|             .performClick() | ||||
| 
 | ||||
|         assert(save) | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,6 @@ package be.ugent.sel.studeez.common.composable | |||
| 
 | ||||
| import androidx.compose.animation.core.animateFloat | ||||
| import androidx.compose.animation.core.updateTransition | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.FloatingActionButton | ||||
| import androidx.compose.material.Icon | ||||
|  |  | |||
|  | @ -0,0 +1,22 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| 
 | ||||
| @Composable | ||||
| fun FormComposable( | ||||
|     title: String, | ||||
|     popUp: () -> Unit, | ||||
|     content: @Composable () -> Unit, | ||||
| ) { | ||||
|     SecondaryScreenTemplate(title = title, popUp = popUp) { | ||||
|         Box( | ||||
|             modifier = Modifier.verticalScroll(rememberScrollState()), | ||||
|         ) { | ||||
|             content() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,39 @@ | |||
| package be.ugent.sel.studeez.common.composable | ||||
| 
 | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.painter.Painter | ||||
| import androidx.compose.ui.unit.dp | ||||
| 
 | ||||
| @Composable | ||||
| fun ImageBackgroundButton( | ||||
|     paint: Painter, | ||||
|     str: String, | ||||
|     background2: Color, | ||||
|     setBackground1: (Color) -> Unit, | ||||
|     setBackground2: (Color) -> Unit | ||||
| ) { | ||||
|     Image( | ||||
|         painter = paint, | ||||
|         str, | ||||
|         modifier = Modifier | ||||
|             .clickable { | ||||
|                 if (background2 == Color.Transparent) { | ||||
|                     setBackground1(Color.LightGray) | ||||
|                     setBackground2(Color.Transparent) | ||||
|                 } else { | ||||
|                     setBackground2(Color.Transparent) | ||||
|                 } | ||||
|             } | ||||
|             .border( | ||||
|                 width = 2.dp, | ||||
|                 color = background2, | ||||
|                 shape = RoundedCornerShape(16.dp) | ||||
|             ) | ||||
|     ) | ||||
| } | ||||
|  | @ -3,7 +3,6 @@ package be.ugent.sel.studeez.common.composable | |||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.text.KeyboardActions | ||||
| import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material.* | ||||
| import androidx.compose.material.icons.Icons | ||||
|  | @ -22,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview | |||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | ||||
| import be.ugent.sel.studeez.resources | ||||
| import kotlin.math.sin | ||||
| import be.ugent.sel.studeez.R.drawable as AppIcon | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
|  | @ -47,7 +45,7 @@ fun LabelledInputField( | |||
|     value: String, | ||||
|     onNewValue: (String) -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     singleLine: Boolean = false | ||||
|     singleLine: Boolean = true | ||||
| ) { | ||||
|     OutlinedTextField( | ||||
|         value = value, | ||||
|  | @ -119,7 +117,9 @@ fun LabeledErrorTextField( | |||
|     initialValue: String, | ||||
|     @StringRes label: Int, | ||||
|     singleLine: Boolean = false, | ||||
|     errorText: Int, | ||||
|     isValid: MutableState<Boolean> = remember { mutableStateOf(true) }, | ||||
|     isFirst: MutableState<Boolean> = remember { mutableStateOf(false) }, | ||||
|     @StringRes errorText: Int, | ||||
|     keyboardType: KeyboardType, | ||||
|     predicate: (String) -> Boolean, | ||||
|     onNewCorrectValue: (String) -> Unit | ||||
|  | @ -128,31 +128,28 @@ fun LabeledErrorTextField( | |||
|         mutableStateOf(initialValue) | ||||
|     } | ||||
| 
 | ||||
|     var isValid by remember { | ||||
|         mutableStateOf(predicate(value)) | ||||
|     } | ||||
| 
 | ||||
|     Column { | ||||
|         OutlinedTextField( | ||||
|             modifier = modifier.fieldModifier(), | ||||
|             value = value, | ||||
|             onValueChange = { newText -> | ||||
|                 isFirst.value = false | ||||
|                 value = newText | ||||
|                 isValid = predicate(value) | ||||
|                 if (isValid) { | ||||
|                 isValid.value = predicate(value) | ||||
|                 if (isValid.value) { | ||||
|                     onNewCorrectValue(newText) | ||||
|                 } | ||||
|             }, | ||||
|             singleLine = singleLine, | ||||
|             label = { Text(text = stringResource(id = label)) }, | ||||
|             isError = !isValid, | ||||
|             isError = !isValid.value && !isFirst.value, | ||||
|             keyboardOptions = KeyboardOptions( | ||||
|                 keyboardType = keyboardType, | ||||
|                 imeAction = ImeAction.Done | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         if (!isValid) { | ||||
|         if (!isValid.value && !isFirst.value) { | ||||
|             Text( | ||||
|                 modifier = Modifier.padding(start = 16.dp), | ||||
|                 text = stringResource(id = errorText), | ||||
|  |  | |||
|  | @ -31,9 +31,13 @@ import be.ugent.sel.studeez.R.string as AppText | |||
| fun SubjectEntry( | ||||
|     subject: Subject, | ||||
|     onViewSubject: () -> Unit, | ||||
|     getTaskCount: () -> Flow<Int>, | ||||
|     getCompletedTaskCount: () -> Flow<Int>, | ||||
|     getStudyTime: () -> Flow<Int>, | ||||
| ) { | ||||
|     val studytime by getStudyTime().collectAsState(initial = 0) | ||||
|     val taskCount by getTaskCount().collectAsState(initial = 0) | ||||
|     val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0) | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|  | @ -80,7 +84,7 @@ fun SubjectEntry( | |||
|                                 imageVector = Icons.Default.List, | ||||
|                                 contentDescription = stringResource(id = AppText.tasks) | ||||
|                             ) | ||||
|                             Text(text = "${subject.taskCompletedCount}/${subject.taskCount}") | ||||
|                             Text(text = "${completedTaskCount}/${taskCount}") | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | @ -104,11 +108,11 @@ fun SubjectEntryPreview() { | |||
|         subject = Subject( | ||||
|             name = "Test Subject", | ||||
|             argb_color = 0xFFFFD200, | ||||
|             taskCount = 5, | ||||
|             taskCompletedCount = 2, | ||||
|         ), | ||||
|         onViewSubject = {}, | ||||
|         getStudyTime = { flowOf() } | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -121,6 +125,8 @@ fun OverflowSubjectEntryPreview() { | |||
|             argb_color = 0xFFFFD200, | ||||
|         ), | ||||
|         onViewSubject = {}, | ||||
|         getStudyTime = { flowOf() } | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|     ) | ||||
| } | ||||
|  | @ -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,17 +1,12 @@ | |||
| package be.ugent.sel.studeez.data.local.models.task | ||||
| 
 | ||||
| import com.google.firebase.firestore.DocumentId | ||||
| import com.google.firebase.firestore.Exclude | ||||
| 
 | ||||
| data class Subject( | ||||
|     @DocumentId val id: String = "", | ||||
|     val name: String = "", | ||||
|     val argb_color: Long = 0, | ||||
|     var archived: Boolean = false, | ||||
|     @get:Exclude @set:Exclude | ||||
|     var taskCount: Int = 0, | ||||
|     @get:Exclude @set:Exclude | ||||
|     var taskCompletedCount: Int = 0, | ||||
| ) | ||||
| 
 | ||||
| object SubjectDocument { | ||||
|  |  | |||
|  | @ -6,14 +6,13 @@ class FunctionalPomodoroTimer( | |||
|     val repeats: Int | ||||
| ) : FunctionalTimer(studyTime) { | ||||
| 
 | ||||
|     var breaksRemaining = repeats | ||||
|     var breaksRemaining = repeats - 1 | ||||
|     var isInBreak = false | ||||
| 
 | ||||
|     override fun tick() { | ||||
|         if (hasEnded()) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (hasCurrentCountdownEnded()) { | ||||
|             if (isInBreak) { | ||||
|                 breaksRemaining-- | ||||
|  |  | |||
|  | @ -13,8 +13,10 @@ interface SubjectDAO { | |||
| 
 | ||||
|     fun updateSubject(newSubject: Subject) | ||||
| 
 | ||||
|     suspend fun getTaskCount(subject: Subject): Int | ||||
|     suspend fun getCompletedTaskCount(subject: Subject): Int | ||||
|     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? | ||||
|  |  | |||
|  | @ -2,10 +2,11 @@ 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.AggregateSource | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.Query | ||||
|  | @ -15,6 +16,7 @@ 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, | ||||
|  | @ -26,13 +28,6 @@ class FireBaseSubjectDAO @Inject constructor( | |||
|             .subjectNotArchived() | ||||
|             .snapshots() | ||||
|             .map { it.toObjects(Subject::class.java) } | ||||
|             .map { subjects -> | ||||
|                 subjects.map { subject -> | ||||
|                     subject.taskCount = getTaskCount(subject) | ||||
|                     subject.taskCompletedCount = getCompletedTaskCount(subject) | ||||
|                     subject | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSubject(subjectId: String): Subject? { | ||||
|  | @ -51,23 +46,26 @@ class FireBaseSubjectDAO @Inject constructor( | |||
|         currentUserSubjectsCollection().document(newSubject.id).set(newSubject) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getTaskCount(subject: Subject): Int { | ||||
|         return subjectTasksCollection(subject) | ||||
|     override suspend fun archiveSubject(subject: Subject) { | ||||
|         currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true) | ||||
|         currentUserSubjectsCollection().document(subject.id) | ||||
|             .collection(FireBaseCollections.TASK_COLLECTION) | ||||
|             .taskNotArchived() | ||||
|             .count() | ||||
|             .get(AggregateSource.SERVER) | ||||
|             .await() | ||||
|             .count.toInt() | ||||
|             .get().await() | ||||
|             .documents | ||||
|             .forEach { | ||||
|                 it.reference.update(TaskDocument.archived, true) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getCompletedTaskCount(subject: Subject): Int { | ||||
|         return subjectTasksCollection(subject) | ||||
|             .taskNotArchived() | ||||
|             .taskNotCompleted() | ||||
|             .count() | ||||
|             .get(AggregateSource.SERVER) | ||||
|             .await() | ||||
|             .count.toInt() | ||||
|     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> { | ||||
|  |  | |||
|  | @ -25,9 +25,9 @@ import be.ugent.sel.studeez.screens.settings.SettingsRoute | |||
| import be.ugent.sel.studeez.screens.sign_up.SignUpRoute | ||||
| import be.ugent.sel.studeez.screens.splash.SplashRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.SubjectRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.TaskRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectCreateRoute | ||||
| import be.ugent.sel.studeez.screens.subjects.form.SubjectEditRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.TaskRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute | ||||
| import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute | ||||
| import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute | ||||
|  | @ -51,6 +51,7 @@ fun StudeezNavGraph( | |||
|     val open: (String) -> Unit = { appState.navigate(it) } | ||||
|     val openAndPopUp: (String, String) -> Unit = | ||||
|         { route, popUp -> appState.navigateAndPopUp(route, popUp) } | ||||
|     val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) } | ||||
| 
 | ||||
|     val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp) | ||||
|     val navigationBarActions: NavigationBarActions = | ||||
|  | @ -200,7 +201,7 @@ fun StudeezNavGraph( | |||
| 
 | ||||
|         composable(StudeezDestinations.SESSION_RECAP) { | ||||
|             SessionRecapRoute( | ||||
|                 openAndPopUp = openAndPopUp, | ||||
|                 clearAndNavigate = clearAndNavigate, | ||||
|                 viewModel = hiltViewModel() | ||||
|             ) | ||||
|         } | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.media.MediaPlayer | ||||
| import android.media.RingtoneManager | ||||
| import android.net.Uri | ||||
| import kotlinx.coroutines.delay | ||||
| import javax.inject.Singleton | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
|  | @ -10,9 +13,11 @@ object InvisibleSessionManager { | |||
|     private var viewModel: SessionViewModel? = null | ||||
|     private lateinit var mediaPlayer: MediaPlayer | ||||
| 
 | ||||
|     fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) { | ||||
|     fun setParameters(viewModel: SessionViewModel, context: Context) { | ||||
|         val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|         this.mediaPlayer = MediaPlayer.create(context, uri) | ||||
|         this.mediaPlayer.isLooping = false | ||||
|         this.viewModel = viewModel | ||||
|         this.mediaPlayer = mediaplayer | ||||
|     } | ||||
| 
 | ||||
|     suspend fun updateTimer() { | ||||
|  |  | |||
|  | @ -1,33 +1,24 @@ | |||
| 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 | ||||
| import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreenComposable | ||||
| 
 | ||||
| data class SessionActions( | ||||
|     val getTimer: () -> FunctionalTimer, | ||||
|     val getTask: () -> String, | ||||
|     val startMediaPlayer: () -> Unit, | ||||
|     val releaseMediaPlayer: () -> Unit, | ||||
|     val endSession: () -> Unit | ||||
| ) | ||||
| 
 | ||||
| private fun getSessionActions( | ||||
|     viewModel: SessionViewModel, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     mediaplayer: MediaPlayer, | ||||
| ): SessionActions { | ||||
|     return SessionActions( | ||||
|         getTimer = viewModel::getTimer, | ||||
|         getTask = viewModel::getTask, | ||||
|         endSession = { viewModel.endSession(openAndPopUp) }, | ||||
|         startMediaPlayer = mediaplayer::start, | ||||
|         releaseMediaPlayer = mediaplayer::release, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -37,20 +28,12 @@ fun SessionRoute( | |||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     viewModel: SessionViewModel, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|     val mediaplayer = MediaPlayer.create(context, uri) | ||||
|     mediaplayer.isLooping = false | ||||
| 
 | ||||
|     InvisibleSessionManager.setParameters( | ||||
|         viewModel = viewModel, | ||||
|         mediaplayer = mediaplayer | ||||
|     ) | ||||
|     InvisibleSessionManager.setParameters(viewModel = viewModel, context = LocalContext.current) | ||||
| 
 | ||||
|     val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen(mediaplayer)) | ||||
|     val soundPlayer = SoundPlayer(LocalContext.current) | ||||
|     val sessionActions = getSessionActions(viewModel, openAndPopUp) | ||||
|     val sessionScreen = viewModel.getTimer().accept(GetSessionScreenComposable(soundPlayer, open, sessionActions)) | ||||
| 
 | ||||
|     sessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = getSessionActions(viewModel, openAndPopUp, mediaplayer) | ||||
|     ) | ||||
|     sessionScreen() | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,29 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.media.MediaPlayer | ||||
| import android.media.RingtoneManager | ||||
| 
 | ||||
| class SoundPlayer(private val context: Context) { | ||||
| 
 | ||||
|     var oldValue: Boolean = false | ||||
|     var mediaPlayer: MediaPlayer = initPlayer() | ||||
| 
 | ||||
|     fun playOn(newValue: Boolean) { | ||||
|         if (oldValue != newValue) { | ||||
|             mediaPlayer.start() | ||||
|             mediaPlayer.setOnCompletionListener { | ||||
|                 mediaPlayer = initPlayer() | ||||
|             } | ||||
|             oldValue = newValue | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private fun initPlayer(): MediaPlayer { | ||||
|         return  MediaPlayer.create( | ||||
|             context, | ||||
|             RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -1,150 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| abstract class AbstractSessionScreen { | ||||
| 
 | ||||
|     @Composable | ||||
|     operator fun invoke( | ||||
|         open: (String) -> Unit, | ||||
|         sessionActions: SessionActions, | ||||
|     ) { | ||||
|         Column( | ||||
|             modifier = Modifier.padding(10.dp) | ||||
|         ) { | ||||
|             Timer( | ||||
|                 sessionActions = sessionActions, | ||||
|             ) | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp) | ||||
|             ) { | ||||
|                 TextButton( | ||||
|                     onClick = { | ||||
|                         sessionActions.releaseMediaPlayer | ||||
|                         sessionActions.endSession() | ||||
|                     }, | ||||
|                     modifier = Modifier | ||||
|                         .padding(horizontal = 20.dp) | ||||
|                         .border(1.dp, Color.Red, RoundedCornerShape(32.dp)) | ||||
|                         .background(Color.Transparent) | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = "End session", | ||||
|                         color = Color.Red, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         fontSize = 18.sp, | ||||
|                         modifier = Modifier.padding(1.dp) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun Timer( | ||||
|         sessionActions: SessionActions, | ||||
|     ) { | ||||
|         var tikker by remember { mutableStateOf(false) } | ||||
|         LaunchedEffect(tikker) { | ||||
|             delay(1.seconds) | ||||
|             sessionActions.getTimer().tick() | ||||
|             callMediaPlayer() | ||||
|             tikker = !tikker | ||||
|         } | ||||
| 
 | ||||
|         val hms = sessionActions.getTimer().getHoursMinutesSeconds() | ||||
|         Column { | ||||
|             Text( | ||||
|                 text = hms.toString(), | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp), | ||||
|                 textAlign = TextAlign.Center, | ||||
|                 fontWeight = FontWeight.Bold, | ||||
|                 fontSize = 40.sp, | ||||
|             ) | ||||
| 
 | ||||
|             Text( | ||||
|                 text = motivationString(), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 textAlign = TextAlign.Center, | ||||
|                 fontWeight = FontWeight.Light, | ||||
|                 fontSize = 30.sp | ||||
|             ) | ||||
| 
 | ||||
|             MidSection() | ||||
| 
 | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(50.dp) | ||||
|             ) { | ||||
|                 Box( | ||||
|                     contentAlignment = Alignment.Center, | ||||
|                     modifier = Modifier | ||||
|                         .padding(16.dp) | ||||
|                         .background(Color.Blue, RoundedCornerShape(32.dp)) | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = sessionActions.getTask(), | ||||
|                         color = Color.White, | ||||
|                         fontSize = 18.sp, | ||||
|                         fontWeight = FontWeight.Bold, | ||||
|                         modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     abstract fun motivationString(): String | ||||
| 
 | ||||
|     @Composable | ||||
|     open fun MidSection() { | ||||
|         // Default has no midsection, unless overwritten. | ||||
|     } | ||||
| 
 | ||||
|     abstract fun callMediaPlayer() | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun TimerPreview() { | ||||
|     val sessionScreen = object : AbstractSessionScreen() { | ||||
|         @Composable | ||||
|         override fun motivationString(): String = "Test" | ||||
|         override fun callMediaPlayer() {} | ||||
| 
 | ||||
|     } | ||||
|     sessionScreen.Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}, {})) | ||||
| } | ||||
|  | @ -1,93 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.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.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.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| class BreakSessionScreen( | ||||
|     private val funPomoDoroTimer: FunctionalPomodoroTimer, | ||||
|     private var mediaplayer: MediaPlayer? | ||||
| ): AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun MidSection() { | ||||
|         Dots() | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun Dots() { | ||||
|         Row( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|             horizontalArrangement = Arrangement.Center, | ||||
|         ) { | ||||
|             repeat(funPomoDoroTimer.repeats - funPomoDoroTimer.breaksRemaining) { | ||||
|                 Dot(color = Color.DarkGray) | ||||
|             } | ||||
|             if (!funPomoDoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray) | ||||
|             repeat(funPomoDoroTimer.breaksRemaining - 1) { | ||||
|                 Dot(color = Color.Gray) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     private fun Dot(color: Color) { | ||||
|         Box(modifier = Modifier | ||||
|             .padding(5.dp) | ||||
|             .size(10.dp) | ||||
|             .clip(CircleShape) | ||||
|             .background(color)) | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         if (funPomoDoroTimer.isInBreak) { | ||||
|             return resources().getString(AppText.state_take_a_break) | ||||
|         } | ||||
| 
 | ||||
|         if (funPomoDoroTimer.hasEnded()) { | ||||
|             return resources().getString(AppText.state_done) | ||||
|         } | ||||
| 
 | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() { | ||||
|         if (funPomoDoroTimer.hasEnded()) { | ||||
|             mediaplayer?.let { it: MediaPlayer -> | ||||
|                 it.setOnCompletionListener { | ||||
|                     it.release() | ||||
|                     mediaplayer = null | ||||
|                 } | ||||
|                 it.start() | ||||
|             } | ||||
|         } else if (funPomoDoroTimer.hasCurrentCountdownEnded()) { | ||||
|             mediaplayer?.start() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun MidsectionPreview() { | ||||
|     val funPomoDoroTimer = FunctionalPomodoroTimer(15, 60, 5) | ||||
|     val breakSessionScreen = BreakSessionScreen(funPomoDoroTimer, MediaPlayer()) | ||||
|     breakSessionScreen.MidSection() | ||||
| } | ||||
|  | @ -0,0 +1,79 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| @Composable | ||||
| fun BreakSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     pomodoroTimer: FunctionalPomodoroTimer, | ||||
|     soundPlayer: SoundPlayer, | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = sessionActions, | ||||
|         midSection = { Dots(pomodoroTimer = pomodoroTimer) }, | ||||
|         callMediaPlayer = { soundPlayer.playOn(pomodoroTimer.hasCurrentCountdownEnded()) }, | ||||
|         motivationString = { motivationString (pomodoroTimer = pomodoroTimer) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun Dots(pomodoroTimer: FunctionalPomodoroTimer): Int { | ||||
|     Row( | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|         horizontalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         if (pomodoroTimer.hasEnded()) { | ||||
|             repeat(pomodoroTimer.repeats) { | ||||
|                 Dot(Color.Green) | ||||
|             } | ||||
|         } else { | ||||
|             repeat(pomodoroTimer.repeats - pomodoroTimer.breaksRemaining - 1) { | ||||
|                 Dot(color = Color.DarkGray) | ||||
|             } | ||||
|             if (!pomodoroTimer.isInBreak) Dot(Color.Green) else Dot(Color.DarkGray) | ||||
|             repeat(pomodoroTimer.breaksRemaining) { | ||||
|                 Dot(color = Color.Gray) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return pomodoroTimer.breaksRemaining | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun Dot(color: Color) { | ||||
|     Box(modifier = Modifier | ||||
|         .padding(5.dp) | ||||
|         .size(10.dp) | ||||
|         .clip(CircleShape) | ||||
|         .background(color)) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(pomodoroTimer: FunctionalPomodoroTimer): String { | ||||
|     if (pomodoroTimer.isInBreak) { | ||||
|         return resources().getString(R.string.state_take_a_break) | ||||
|     } | ||||
| 
 | ||||
|     if (pomodoroTimer.hasEnded()) { | ||||
|         return resources().getString(R.string.state_done) | ||||
|     } | ||||
| 
 | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,35 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| 
 | ||||
| class CustomSessionScreen( | ||||
|     private val functionalTimer: FunctionalCustomTimer, | ||||
|     private var mediaplayer: MediaPlayer? | ||||
| ): AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         if (functionalTimer.hasEnded()) { | ||||
|             return resources().getString(AppText.state_done) | ||||
|         } | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() { | ||||
|         if (functionalTimer.hasEnded()) { | ||||
|             mediaplayer?.let { it: MediaPlayer -> | ||||
|                 it.setOnCompletionListener { | ||||
|                     it.release() | ||||
|                     mediaplayer = null | ||||
|                 } | ||||
|                 it.start() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,32 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| @Composable | ||||
| fun CustomTimerSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     customTimer: FunctionalCustomTimer, | ||||
|     soundPlayer: SoundPlayer | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         callMediaPlayer = { soundPlayer.playOn(customTimer.hasEnded()) }, | ||||
|         sessionActions = sessionActions | ||||
|     ) { | ||||
|         motivationString(customTimer = customTimer) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(customTimer: FunctionalCustomTimer): String { | ||||
|     if (customTimer.hasEnded()) { | ||||
|         return resources().getString(R.string.state_done) | ||||
|     } | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,16 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| 
 | ||||
| class EndlessSessionScreen : AbstractSessionScreen() { | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun motivationString(): String { | ||||
|         return resources().getString(AppText.state_focus) | ||||
|     } | ||||
| 
 | ||||
|     override fun callMediaPlayer() {} | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.resources | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| 
 | ||||
| @Composable | ||||
| fun EndlessTimerSessionScreenComposable( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
| ) { | ||||
|     SessionScreen( | ||||
|         open = open, | ||||
|         sessionActions = sessionActions | ||||
|     ) { | ||||
|         motivationString() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| private fun motivationString(): String { | ||||
|     return resources().getString(R.string.state_focus) | ||||
| } | ||||
|  | @ -1,18 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor | ||||
| 
 | ||||
| class GetSessionScreen(private val mediaplayer: MediaPlayer?) : FunctionalTimerVisitor<AbstractSessionScreen> { | ||||
|     override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen = | ||||
|         CustomSessionScreen(functionalCustomTimer, mediaplayer) | ||||
| 
 | ||||
|     override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen = | ||||
|         EndlessSessionScreen() | ||||
| 
 | ||||
|     override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen = | ||||
|         BreakSessionScreen(functionalPomodoroTimer, mediaplayer) | ||||
| } | ||||
|  | @ -0,0 +1,47 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import be.ugent.sel.studeez.screens.session.SoundPlayer | ||||
| 
 | ||||
| class GetSessionScreenComposable( | ||||
|     private val soundPlayer: SoundPlayer, | ||||
|     private val open: (String) -> Unit, | ||||
|     private val sessionActions: SessionActions | ||||
|     ) : | ||||
|     FunctionalTimerVisitor<@Composable () -> Unit> { | ||||
| 
 | ||||
|     override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): @Composable () -> Unit { | ||||
|         return { CustomTimerSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|                 soundPlayer = soundPlayer, | ||||
|                 customTimer = functionalCustomTimer, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): @Composable () -> Unit { | ||||
|         return { | ||||
|             EndlessTimerSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): @Composable () -> Unit { | ||||
|         return { | ||||
|             BreakSessionScreenComposable( | ||||
|                 open = open, | ||||
|                 sessionActions = sessionActions, | ||||
|                 soundPlayer = soundPlayer, | ||||
|                 pomodoroTimer = functionalPomodoroTimer | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,73 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.border | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionScreen( | ||||
|     open: (String) -> Unit, | ||||
|     sessionActions: SessionActions, | ||||
|     callMediaPlayer: () -> Unit = {}, | ||||
|     midSection: @Composable () -> Int = {0}, | ||||
|     motivationString: @Composable () -> String, | ||||
| 
 | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier.padding(10.dp) | ||||
|     ) { | ||||
|         Timer( | ||||
|             sessionActions = sessionActions, | ||||
|             callMediaPlayer = callMediaPlayer, | ||||
|             motivationString = motivationString, | ||||
|             MidSection = midSection | ||||
|         ) | ||||
|         Box( | ||||
|             contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(50.dp) | ||||
|         ) { | ||||
|             EndSessionButton(sessionActions = sessionActions) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EndSessionButton(sessionActions: SessionActions) { | ||||
|     TextButton( | ||||
|         onClick = { | ||||
|             sessionActions.endSession() | ||||
|         }, | ||||
|         modifier = Modifier | ||||
|             .padding(horizontal = 20.dp) | ||||
|             .border(1.dp, Color.Red, RoundedCornerShape(32.dp)) | ||||
|             .background(Color.Transparent) | ||||
|     ) { | ||||
|         EndsessionText() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EndsessionText() { | ||||
|     Text( | ||||
|         text = "End session", | ||||
|         color = Color.Red, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         fontSize = 18.sp, | ||||
|         modifier = Modifier.padding(1.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,95 @@ | |||
| package be.ugent.sel.studeez.screens.session.sessionScreens | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.screens.session.SessionActions | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
| 
 | ||||
| @Composable | ||||
| fun Timer( | ||||
|     sessionActions: SessionActions, | ||||
|     callMediaPlayer: () -> Unit, | ||||
|     motivationString: @Composable () -> String, | ||||
|     MidSection: @Composable () -> Int | ||||
| ) { | ||||
|     var tikker by remember { mutableStateOf(false) } | ||||
|     LaunchedEffect(tikker) { | ||||
|         delay(1.seconds) | ||||
|         sessionActions.getTimer().tick() | ||||
|         callMediaPlayer() | ||||
|         tikker = !tikker | ||||
|     } | ||||
| 
 | ||||
|     val hms = sessionActions.getTimer().getHoursMinutesSeconds() | ||||
|     Column { | ||||
| 
 | ||||
|         TimerClock(hms) | ||||
|         MotivationText(text = motivationString()) | ||||
|         MidSection() | ||||
| 
 | ||||
|         Box( | ||||
|             contentAlignment = Alignment.Center, modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(50.dp) | ||||
|         ) { | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, | ||||
|                 modifier = Modifier | ||||
|                     .padding(16.dp) | ||||
|                     .background(Color.Blue, RoundedCornerShape(32.dp)) | ||||
|             ) { | ||||
|                 TaskText(taskName = sessionActions.getTask()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerClock(hms: HoursMinutesSeconds) { | ||||
|     Text( | ||||
|         text = hms.toString(), | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(50.dp), | ||||
|         textAlign = TextAlign.Center, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         fontSize = 40.sp, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun MotivationText(text: String) { | ||||
|     Text( | ||||
|         text = text, | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         textAlign = TextAlign.Center, | ||||
|         fontWeight = FontWeight.Light, | ||||
|         fontSize = 30.sp | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TaskText(taskName: String) { | ||||
|     Text( | ||||
|         text = taskName, | ||||
|         color = Color.White, | ||||
|         fontSize = 18.sp, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -1,13 +1,24 @@ | |||
| package be.ugent.sel.studeez.screens.session_recap | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.ButtonDefaults | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.res.painterResource | ||||
| import androidx.compose.ui.res.stringResource | ||||
| 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.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.ImageBackgroundButton | ||||
| 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 | ||||
|  | @ -21,24 +32,24 @@ data class SessionRecapActions( | |||
| 
 | ||||
| fun getSessionRecapActions( | ||||
|     viewModel: SessionRecapViewModel, | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     clearAndNavigate: (String) -> Unit, | ||||
| ): SessionRecapActions { | ||||
|     return SessionRecapActions( | ||||
|         viewModel::getSessionReport, | ||||
|         {viewModel.saveSession(openAndPopUp)}, | ||||
|         {viewModel.discardSession(openAndPopUp)} | ||||
|         { viewModel.saveSession(clearAndNavigate) }, | ||||
|         { viewModel.discardSession(clearAndNavigate) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun SessionRecapRoute( | ||||
|     openAndPopUp: (String, String) -> Unit, | ||||
|     clearAndNavigate: (String) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
|     viewModel: SessionRecapViewModel, | ||||
| ) { | ||||
|     SessionRecapScreen( | ||||
|         modifier = modifier, | ||||
|         getSessionRecapActions(viewModel, openAndPopUp) | ||||
|         getSessionRecapActions(viewModel, clearAndNavigate) | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -47,21 +58,88 @@ fun SessionRecapScreen(modifier: Modifier, sessionRecapActions: SessionRecapActi | |||
|     val sessionReport: SessionReport = sessionRecapActions.getSessionReport() | ||||
|     val studyTime: Int = sessionReport.studyTime | ||||
|     val hms: HoursMinutesSeconds = Time(studyTime).getAsHMS() | ||||
|     val (background1, setBackground1) = remember { mutableStateOf(Color.Transparent) } | ||||
|     val (background2, setBackground2) = remember { mutableStateOf(Color.Transparent) } | ||||
|     Column( | ||||
|         modifier = modifier | ||||
|             .fillMaxWidth() | ||||
|             .fillMaxHeight() | ||||
|             .padding(16.dp), | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         verticalArrangement = Arrangement.SpaceBetween | ||||
|     ) { | ||||
|         Text(text = "You studied: $hms") | ||||
|         Text( | ||||
|             text = stringResource(R.string.congrats, hms), | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth(), | ||||
|             textAlign = TextAlign.Center, | ||||
|             fontWeight = FontWeight.Light, | ||||
|             fontSize = 30.sp, | ||||
| 
 | ||||
|         BasicButton( | ||||
|             R.string.save, Modifier.basicButton() | ||||
|             ) | ||||
| 
 | ||||
|         Column( | ||||
|             modifier = Modifier.fillMaxWidth() | ||||
|         ) { | ||||
|             sessionRecapActions.saveSession() | ||||
|             Text( | ||||
|                 text = stringResource(R.string.how_did_it_go), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 textAlign = TextAlign.Center, | ||||
|                 fontWeight = FontWeight.Light, | ||||
|                 fontSize = 30.sp | ||||
|             ) | ||||
| 
 | ||||
|             Row( | ||||
|                 horizontalArrangement = Arrangement.Center, | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .align(Alignment.CenterHorizontally) | ||||
|             ) { | ||||
|                 ImageBackgroundButton( | ||||
|                     paint = painterResource(id = R.drawable.mood_1), | ||||
|                     str = stringResource(id = R.string.good), | ||||
|                     background2 = background2, | ||||
|                     setBackground1 = setBackground2, | ||||
|                     setBackground2 = setBackground1 | ||||
|                 ) | ||||
| 
 | ||||
|                 ImageBackgroundButton( | ||||
|                     paint = painterResource(id = R.drawable.mood_2), | ||||
|                     str = stringResource(id = R.string.bad), | ||||
|                     background2 = background1, | ||||
|                     setBackground1 = setBackground1, | ||||
|                     setBackground2 = setBackground2 | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         BasicButton( | ||||
|             R.string.discard, Modifier.basicButton(), | ||||
|             colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) | ||||
|         ) { | ||||
|             sessionRecapActions.discardSession() | ||||
| 
 | ||||
|         Column { | ||||
|             BasicButton( | ||||
|                 R.string.save, Modifier.basicButton() | ||||
|             ) { | ||||
|                 sessionRecapActions.saveSession() | ||||
|             } | ||||
|             BasicButton( | ||||
|                 R.string.discard, Modifier.basicButton(), | ||||
|                 colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) | ||||
|             ) { | ||||
|                 sessionRecapActions.discardSession() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun SessionRecapScreenPreview() { | ||||
|     SessionRecapScreen( | ||||
|         modifier = Modifier, | ||||
|         sessionRecapActions = SessionRecapActions( | ||||
|             { SessionReport( | ||||
|                 studyTime = 100, | ||||
|             ) }, | ||||
|             {}, | ||||
|             {}, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  |  | |||
|  | @ -24,15 +24,15 @@ class SessionRecapViewModel @Inject constructor( | |||
|         return selectedSessionReport() | ||||
|     } | ||||
| 
 | ||||
|     fun saveSession(open: (String, String) -> Unit) { | ||||
|     fun saveSession(open: (String) -> Unit) { | ||||
|         sessionDAO.saveSession(getSessionReport()) | ||||
|         val newTask = | ||||
|             selectedTask().copy(time = selectedTask().time + selectedSessionReport().studyTime) | ||||
|         taskDAO.updateTask(newTask) | ||||
|         open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) | ||||
|         open(StudeezDestinations.HOME_SCREEN) | ||||
|     } | ||||
| 
 | ||||
|     fun discardSession(open: (String, String) -> Unit) { | ||||
|         open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP) | ||||
|     fun discardSession(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.HOME_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -36,6 +36,8 @@ fun SubjectRoute( | |||
|         navigationBarActions = navigationBarActions, | ||||
|         onAddSubject = { viewModel.onAddSubject(open) }, | ||||
|         onViewSubject = { viewModel.onViewSubject(it, open) }, | ||||
|         getTaskCount = viewModel::getTaskCount, | ||||
|         getCompletedTaskCount = viewModel::getCompletedTaskCount, | ||||
|         getStudyTime = viewModel::getStudyTime, | ||||
|         uiState, | ||||
|     ) | ||||
|  | @ -47,6 +49,8 @@ fun SubjectScreen( | |||
|     navigationBarActions: NavigationBarActions, | ||||
|     onAddSubject: () -> Unit, | ||||
|     onViewSubject: (Subject) -> Unit, | ||||
|     getTaskCount: (Subject) -> Flow<Int>, | ||||
|     getCompletedTaskCount: (Subject) -> Flow<Int>, | ||||
|     getStudyTime: (Subject) -> Flow<Int>, | ||||
|     uiState: SubjectUiState, | ||||
| ) { | ||||
|  | @ -76,6 +80,8 @@ fun SubjectScreen( | |||
|                             SubjectEntry( | ||||
|                                 subject = it, | ||||
|                                 onViewSubject = { onViewSubject(it) }, | ||||
|                                 getTaskCount = { getTaskCount(it) }, | ||||
|                                 getCompletedTaskCount = { getCompletedTaskCount(it) }, | ||||
|                                 getStudyTime = { getStudyTime(it) }, | ||||
|                             ) | ||||
|                         } | ||||
|  | @ -94,13 +100,14 @@ fun SubjectScreenPreview() { | |||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||
|         onAddSubject = {}, | ||||
|         onViewSubject = {}, | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|         uiState = SubjectUiState.Succes( | ||||
|             listOf( | ||||
|                 Subject( | ||||
|                     name = "Test Subject", | ||||
|                     argb_color = 0xFFFFD200, | ||||
|                     taskCount = 5, taskCompletedCount = 2, | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|  | @ -115,7 +122,9 @@ fun SubjectScreenLoadingPreview() { | |||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||
|         onAddSubject = {}, | ||||
|         onViewSubject = {}, | ||||
|         getTaskCount = { flowOf() }, | ||||
|         getCompletedTaskCount = { flowOf() }, | ||||
|         getStudyTime = { flowOf() }, | ||||
|         uiState = SubjectUiState.Loading | ||||
|         uiState = SubjectUiState.Loading, | ||||
|     ) | ||||
| } | ||||
|  | @ -30,6 +30,14 @@ class SubjectViewModel @Inject constructor( | |||
|         open(StudeezDestinations.ADD_SUBJECT_FORM) | ||||
|     } | ||||
| 
 | ||||
|     fun getTaskCount(subject: Subject): Flow<Int> { | ||||
|         return subjectDAO.getTaskCount(subject) | ||||
|     } | ||||
| 
 | ||||
|     fun getCompletedTaskCount(subject: Subject): Flow<Int> { | ||||
|         return subjectDAO.getCompletedTaskCount(subject) | ||||
|     } | ||||
| 
 | ||||
|     fun getStudyTime(subject: Subject): Flow<Int> { | ||||
|         return subjectDAO.getStudyTime(subject) | ||||
|     } | ||||
|  |  | |||
|  | @ -2,20 +2,27 @@ package be.ugent.sel.studeez.screens.subjects.form | |||
| 
 | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material.OutlinedTextField | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.ButtonDefaults | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| 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 | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.FormComposable | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | ||||
| import be.ugent.sel.studeez.common.ext.generateRandomArgb | ||||
| import be.ugent.sel.studeez.resources | ||||
| import kotlinx.coroutines.launch | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
|  | @ -31,7 +38,7 @@ fun SubjectCreateRoute( | |||
|         uiState = uiState, | ||||
|         onConfirm = { viewModel.onCreate(openAndPopUp) }, | ||||
|         onNameChange = viewModel::onNameChange, | ||||
|         onColorChange = {}, | ||||
|         onColorChange = viewModel::onColorChange, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -42,16 +49,19 @@ fun SubjectEditRoute( | |||
|     viewModel: SubjectEditFormViewModel, | ||||
| ) { | ||||
|     val uiState by viewModel.uiState | ||||
|     val coroutineScope = rememberCoroutineScope() | ||||
|     SubjectForm( | ||||
|         title = AppText.edit_subject, | ||||
|         goBack = goBack, | ||||
|         uiState = uiState, | ||||
|         onConfirm = { viewModel.onEdit(openAndPopUp) }, | ||||
|         onNameChange = viewModel::onNameChange, | ||||
|         onColorChange = {}, | ||||
|         onColorChange = viewModel::onColorChange, | ||||
|     ) { | ||||
|         DeleteButton(text = AppText.delete_subject) { | ||||
|             viewModel.onDelete(openAndPopUp) | ||||
|             coroutineScope.launch { | ||||
|                 viewModel.onDelete(openAndPopUp) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -63,21 +73,21 @@ fun SubjectForm( | |||
|     uiState: SubjectFormUiState, | ||||
|     onConfirm: () -> Unit, | ||||
|     onNameChange: (String) -> Unit, | ||||
|     onColorChange: (Color) -> Unit, | ||||
|     onColorChange: (Long) -> Unit, | ||||
|     extraButton: @Composable () -> Unit = {}, | ||||
| ) { | ||||
|     SecondaryScreenTemplate( | ||||
|     FormComposable( | ||||
|         title = resources().getString(title), | ||||
|         popUp = goBack, | ||||
|     ) { | ||||
|         Column { | ||||
|             OutlinedTextField( | ||||
|             LabelledInputField( | ||||
|                 singleLine = true, | ||||
|                 value = uiState.name, | ||||
|                 onValueChange = onNameChange, | ||||
|                 placeholder = { Text(stringResource(id = AppText.name)) }, | ||||
|                 modifier = Modifier.fieldModifier(), | ||||
|                 onNewValue = onNameChange, | ||||
|                 label = AppText.name, | ||||
|             ) | ||||
|             ColorPicker(onColorChange, uiState) | ||||
|             BasicButton( | ||||
|                 text = AppText.confirm, | ||||
|                 modifier = Modifier.basicButton(), | ||||
|  | @ -88,6 +98,24 @@ fun SubjectForm( | |||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun ColorPicker( | ||||
|     onColorChange: (Long) -> Unit, | ||||
|     uiState: SubjectFormUiState, | ||||
| ) { | ||||
|     Button( | ||||
|         onClick = { onColorChange(Color.generateRandomArgb()) }, | ||||
|         modifier = Modifier.fieldModifier(), | ||||
|         colors = ButtonDefaults.buttonColors( | ||||
|             backgroundColor = Color(uiState.color), | ||||
|             contentColor = Color.White, | ||||
|         ), | ||||
|         shape = RoundedCornerShape(4.dp), | ||||
|     ) { | ||||
|         Text(text = stringResource(id = AppText.regenerate_color)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun AddSubjectFormPreview() { | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| package be.ugent.sel.studeez.screens.subjects.form | ||||
| 
 | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import be.ugent.sel.studeez.common.ext.generateRandomArgb | ||||
| 
 | ||||
| data class SubjectFormUiState( | ||||
|     val name: String = "", | ||||
|     val color: Long = 0xFFFFD200, | ||||
|     val color: Long = Color.generateRandomArgb(), | ||||
| ) | ||||
|  | @ -6,6 +6,7 @@ import be.ugent.sel.studeez.data.SelectedSubject | |||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.SubjectDAO | ||||
| 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 | ||||
|  | @ -59,6 +60,7 @@ class SubjectCreateFormViewModel @Inject constructor( | |||
| @HiltViewModel | ||||
| class SubjectEditFormViewModel @Inject constructor( | ||||
|     subjectDAO: SubjectDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|     selectedSubject: SelectedSubject, | ||||
|     logService: LogService, | ||||
| ) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) { | ||||
|  | @ -69,17 +71,19 @@ class SubjectEditFormViewModel @Inject constructor( | |||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|     fun onDelete(openAndPopUp: (String, String) -> Unit) { | ||||
|         subjectDAO.updateSubject(selectedSubject().copy(archived = true)) | ||||
|     suspend fun onDelete(openAndPopUp: (String, String) -> Unit) { | ||||
|         subjectDAO.archiveSubject(selectedSubject()) | ||||
|         openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM) | ||||
|     } | ||||
| 
 | ||||
|     fun onEdit(openAndPopUp: (String, String) -> Unit) { | ||||
|         val newSubject = selectedSubject().copy( | ||||
|             name = name, | ||||
|             argb_color = color, | ||||
|         selectedSubject.set( | ||||
|             selectedSubject().copy( | ||||
|                 name = name, | ||||
|                 argb_color = color, | ||||
|             ) | ||||
|         ) | ||||
|         subjectDAO.updateSubject(newSubject) | ||||
|         subjectDAO.updateSubject(selectedSubject()) | ||||
|         openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM) | ||||
|     } | ||||
| } | ||||
|  | @ -11,7 +11,7 @@ import androidx.compose.ui.res.stringResource | |||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.FormComposable | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.common.ext.fieldModifier | ||||
| import be.ugent.sel.studeez.resources | ||||
|  | @ -62,7 +62,7 @@ fun TaskForm( | |||
|     onNameChange: (String) -> Unit, | ||||
|     extraButton: @Composable () -> Unit = {} | ||||
| ) { | ||||
|     SecondaryScreenTemplate( | ||||
|     FormComposable( | ||||
|         title = resources().getString(title), | ||||
|         popUp = goBack, | ||||
|     ) { | ||||
|  |  | |||
|  | @ -1,55 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.timer_form | ||||
| 
 | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.PomodoroTimerInfo | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerAddRoute( | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
|     TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.add_timer) { | ||||
|         viewModel.saveTimer(it, goBack = popUp) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerEditRoute( | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
|     TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.edit_timer) { | ||||
|         viewModel.editTimer(it, goBack = popUp) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerFormScreen( | ||||
|     popUp: () -> Unit, | ||||
|     getTimerInfo: () -> TimerInfo, | ||||
|     @StringRes label: Int, | ||||
|     onConfirmClick: (TimerInfo) -> Unit | ||||
| ) { | ||||
|     val timerFormScreen = getTimerInfo().accept(GetTimerFormScreen()) | ||||
| 
 | ||||
|     SecondaryScreenTemplate(title = stringResource(id = label), popUp = popUp) { | ||||
|         timerFormScreen(onConfirmClick) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun AddTimerPreview() { | ||||
|     TimerFormScreen( | ||||
|         popUp = {  }, | ||||
|         getTimerInfo = { PomodoroTimerInfo("", "", 0, 0, 0) }, | ||||
|         label = AppText.add_timer, | ||||
|         onConfirmClick = {} | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,68 @@ | |||
| package be.ugent.sel.studeez.screens.timer_form | ||||
| 
 | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import be.ugent.sel.studeez.common.composable.DeleteButton | ||||
| import be.ugent.sel.studeez.common.composable.FormComposable | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerAddRoute( | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
| 
 | ||||
| 
 | ||||
|     TimerFormScreen( | ||||
|         popUp = popUp, | ||||
|         getTimerInfo = viewModel::getTimerInfo, | ||||
|         extraButton= { }, | ||||
|         AppText.add_timer | ||||
|     ) { | ||||
|         viewModel.saveTimer(it, goBack = {popUp(); popUp()}) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerEditRoute( | ||||
|     popUp: () -> Unit, | ||||
|     viewModel: TimerFormViewModel | ||||
| ) { | ||||
| 
 | ||||
|     @Composable | ||||
|     fun deleteButton() { | ||||
|         DeleteButton(text = AppText.delete_timer) { | ||||
|             viewModel.deleteTimer(viewModel.getTimerInfo(), popUp) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     TimerFormScreen( | ||||
|         popUp = popUp, | ||||
|         getTimerInfo = viewModel::getTimerInfo, | ||||
|         extraButton= { deleteButton() }, | ||||
|         AppText.edit_timer | ||||
|     ) { | ||||
|         viewModel.editTimer(it, goBack = popUp) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun TimerFormScreen( | ||||
|     popUp: () -> Unit, | ||||
|     getTimerInfo: () -> TimerInfo, | ||||
|     extraButton: @Composable () -> Unit, | ||||
|     @StringRes label: Int, | ||||
|     onConfirmClick: (TimerInfo) -> Unit | ||||
| ) { | ||||
|     val timerFormScreen = getTimerInfo().accept(GetTimerFormScreen()) | ||||
| 
 | ||||
|     FormComposable( | ||||
|         title = stringResource(id = label), | ||||
|         popUp = popUp | ||||
|     ) { | ||||
|         timerFormScreen(onConfirmClick, extraButton) | ||||
|     } | ||||
| } | ||||
|  | @ -23,6 +23,11 @@ class TimerFormViewModel @Inject constructor( | |||
|         goBack() | ||||
|     } | ||||
| 
 | ||||
|     fun deleteTimer(timerInfo: TimerInfo, goBack: () -> Unit) { | ||||
|         timerDAO.deleteTimer(timerInfo) | ||||
|         goBack() | ||||
|     } | ||||
| 
 | ||||
|     fun saveTimer(timerInfo: TimerInfo, goBack: () -> Unit) { | ||||
|         timerDAO.saveTimer(timerInfo) | ||||
|         goBack() | ||||
|  |  | |||
|  | @ -1,69 +1,84 @@ | |||
| package be.ugent.sel.studeez.screens.timer_form.form_screens | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.runtime.Composable | ||||
| 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.runtime.* | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.input.KeyboardType | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.LabelledInputField | ||||
| import be.ugent.sel.studeez.common.composable.LabeledErrorTextField | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.common.snackbar.SnackbarManager | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| abstract class AbstractTimerFormScreen(private val timerInfo: TimerInfo) { | ||||
| 
 | ||||
|     protected val valids = mutableMapOf( | ||||
|         "name" to mutableStateOf(textPredicate(timerInfo.name)), | ||||
|         "description" to mutableStateOf(textPredicate(timerInfo.description)) | ||||
|     ) | ||||
| 
 | ||||
|     protected val firsts = mutableMapOf( | ||||
|         "name" to mutableStateOf(true), | ||||
|         "description" to mutableStateOf(true) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|     @Composable | ||||
|     operator fun invoke(onSaveClick: (TimerInfo) -> Unit) { | ||||
|     operator fun invoke( | ||||
|         onSaveClick: (TimerInfo) -> Unit, | ||||
|         extraButton: @Composable () -> Unit = {}, | ||||
|     ) { | ||||
| 
 | ||||
|         var name by remember { mutableStateOf(timerInfo.name) } | ||||
|         var description by remember { mutableStateOf(timerInfo.description) } | ||||
| 
 | ||||
|         // This shall rerun whenever name and description change | ||||
|         timerInfo.name = name | ||||
|         timerInfo.description = description | ||||
| 
 | ||||
|         Column( | ||||
|             verticalArrangement = Arrangement.SpaceBetween, | ||||
|             modifier = Modifier.fillMaxHeight().verticalScroll(rememberScrollState()), | ||||
|         ) { | ||||
|             Column( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 horizontalAlignment = Alignment.CenterHorizontally | ||||
|             ) { | ||||
| 
 | ||||
|                 // Fields that every timer shares (ommited id) | ||||
|                 LabelledInputField( | ||||
|                     value = name, | ||||
|                     onNewValue = { name = it }, | ||||
|                     label = R.string.name | ||||
|                 ) | ||||
| 
 | ||||
|                 LabelledInputField( | ||||
|                     value = description, | ||||
|                     onNewValue = { description = it }, | ||||
|                     label = AppText.description, | ||||
|                     singleLine = false | ||||
|                 ) | ||||
| 
 | ||||
|                 ExtraFields() | ||||
|         Column { | ||||
| 
 | ||||
|             // Fields that every timer shares (ommited id) | ||||
|             LabeledErrorTextField( | ||||
|                 initialValue = timerInfo.name, | ||||
|                 label = R.string.name, | ||||
|                 errorText = AppText.name_error, | ||||
|                 isValid = valids.getValue("name"), | ||||
|                 isFirst = firsts.getValue("name"), | ||||
|                 keyboardType = KeyboardType.Text, | ||||
|                 predicate = { it.isNotBlank() } | ||||
|             ) { correctName -> | ||||
|                 timerInfo.name = correctName | ||||
|             } | ||||
| 
 | ||||
|             LabeledErrorTextField( | ||||
|                 initialValue = timerInfo.description, | ||||
|                 label = R.string.description, | ||||
|                 errorText = AppText.description_error, | ||||
|                 isValid = valids.getValue("description"), | ||||
|                 isFirst = firsts.getValue("description"), | ||||
|                 singleLine = false, | ||||
|                 keyboardType = KeyboardType.Text, | ||||
|                 predicate = { textPredicate(it) } | ||||
|             ) { correctName -> | ||||
|                 timerInfo.description = correctName | ||||
|             } | ||||
| 
 | ||||
|             ExtraFields() | ||||
| 
 | ||||
|             BasicButton(R.string.save, Modifier.basicButton()) { | ||||
|                 onSaveClick(timerInfo) | ||||
|                 if (valids.all { it.component2().value }) { // All fields are valid | ||||
|                     onSaveClick(timerInfo) | ||||
|                 } else { | ||||
|                     firsts.map { | ||||
|                         it.component2().value = false | ||||
|                     } // dont mask error because its not been filled out yet | ||||
|                     SnackbarManager.showMessage(AppText.fill_out_error) | ||||
|                 } | ||||
|             } | ||||
|             extraButton() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun textPredicate(text: String): Boolean { | ||||
|         return text.isNotBlank() | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     open fun ExtraFields() { | ||||
|         // By default no extra fields, unless overwritten by subclass. | ||||
|  |  | |||
|  | @ -15,6 +15,8 @@ class BreakTimerFormScreen( | |||
|     private val breakTimerInfo: PomodoroTimerInfo | ||||
| ): AbstractTimerFormScreen(breakTimerInfo) { | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     @Composable | ||||
|     override fun ExtraFields() { | ||||
|         // If the user presses the OK button on the timepicker, the time in the button should change | ||||
|  | @ -26,12 +28,17 @@ class BreakTimerFormScreen( | |||
|             breakTimerInfo.breakTime = newTime | ||||
|         } | ||||
| 
 | ||||
|         valids["repeats"] = remember {mutableStateOf(true)} | ||||
|         firsts["repeats"] = remember { mutableStateOf(true) } | ||||
| 
 | ||||
|         LabeledErrorTextField( | ||||
|             initialValue = breakTimerInfo.repeats.toString(), | ||||
|             label = R.string.repeats, | ||||
|             errorText = AppText.repeats_error, | ||||
|             isValid = valids.getValue("repeats"), | ||||
|             isFirst = firsts.getValue("repeats"), | ||||
|             keyboardType = KeyboardType.Decimal, | ||||
|             predicate = { it.matches(Regex("[1-9]+\\d*")) } | ||||
|             predicate = { isNumber(it) } | ||||
|         ) { correctlyTypedInt -> | ||||
|             breakTimerInfo.repeats = correctlyTypedInt.toInt() | ||||
|         } | ||||
|  | @ -39,6 +46,10 @@ class BreakTimerFormScreen( | |||
|     } | ||||
| } | ||||
| 
 | ||||
| fun isNumber(text: String): Boolean { | ||||
|     return text.matches(Regex("[1-9]+\\d*")) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun BreakEditScreenPreview() { | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package be.ugent.sel.studeez.screens.timer_form.timer_type_select | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
|  | @ -9,6 +8,7 @@ import androidx.compose.ui.Alignment | |||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.hilt.navigation.compose.hiltViewModel | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.* | ||||
|  | @ -38,7 +38,10 @@ fun TimerTypeSelectScreen( | |||
|         ) { | ||||
|             TimerType.values().forEach { timerType -> | ||||
|                 val default: TimerInfo = defaultTimerInfo.getValue(timerType) | ||||
|                 Button(onClick = { viewModel.onTimerTypeChosen(default, open) }) { | ||||
|                 Button( | ||||
|                     onClick = { viewModel.onTimerTypeChosen(default, open) }, | ||||
|                     modifier = Modifier.fillMaxWidth().padding(5.dp) | ||||
|                 ) { | ||||
|                     Text(text = timerType.name) | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/mood_1.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/mood_1.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| <vector android:height="75dp" android:tint="#999999" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="75dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/> | ||||
| </vector> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/mood_2.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/mood_2.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| <vector android:height="75dp" android:tint="#999999" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="75dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,14c-2.33,0 -4.31,1.46 -5.11,3.5h10.22c-0.8,-2.04 -2.78,-3.5 -5.11,-3.5z"/> | ||||
| </vector> | ||||
|  | @ -46,6 +46,7 @@ | |||
|     <string name="delete_subject">Delete Subject</string> | ||||
|     <string name="delete_task">Delete Task</string> | ||||
|     <string name="view_tasks">View</string> | ||||
|     <string name="regenerate_color">Regenerate Color</string> | ||||
| 
 | ||||
|     <!-- Sessions --> | ||||
|     <string name="sessions_temp_description">Looks like you found the sessions screen! In here, your upcoming studying sessions with friends will be listed. You can accept invites or edit your own.</string> <!-- TODO Remove this description line once implemented. --> | ||||
|  | @ -69,8 +70,15 @@ | |||
| 
 | ||||
|     <!-- Timers --> | ||||
|     <string name="timers">Timers</string> | ||||
|     <string name="delete_timer">Delete Timer</string> | ||||
|     <string name="edit">Edit</string> | ||||
|     <string name="add_timer">Add timer</string> | ||||
| 
 | ||||
|     <string name="name_error">Name should not be blank</string> | ||||
|     <string name="description_error">Description should not be blank</string> | ||||
|     <string name="fill_out_error">Fill out all the fields correctly!</string> | ||||
| 
 | ||||
| 
 | ||||
|     <string name="pick_time">Select time</string> | ||||
|     <string name="state_focus">Focus!</string> | ||||
|     <plurals name="state_focus_remaining"> | ||||
|  | @ -149,4 +157,11 @@ | |||
|     <string name="breakTime">Break Time</string> | ||||
|     <string name="repeats">Number of Repeats</string> | ||||
| 
 | ||||
|     <!-- Session Recap --> | ||||
|     <string name="congrats">"Congratulations! You studied: %s"</string> | ||||
|     <string name="how_did_it_go">How did it go?</string> | ||||
|     <string name="good">Good</string> | ||||
|     <string name="bad">Bad</string> | ||||
| 
 | ||||
| 
 | ||||
| </resources> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| package be.ugent.sel.studeez.timer_functional | ||||
| 
 | ||||
| import android.media.MediaPlayer | ||||
| import be.ugent.sel.studeez.data.SelectedSessionReport | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.data.SelectedTimer | ||||
|  | @ -22,13 +21,12 @@ import org.mockito.kotlin.mock | |||
| class InvisibleSessionManagerTest { | ||||
|     private var selectedTimer: SelectedTimer = SelectedTimer() | ||||
|     private lateinit var viewModel: SessionViewModel | ||||
|     private var mediaPlayer: MediaPlayer = mock() | ||||
| 
 | ||||
|     @Test | ||||
|     fun InvisibleEndlessTimerTest() = runTest { | ||||
|         selectedTimer.set(FunctionalEndlessTimer()) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  | @ -50,7 +48,7 @@ class InvisibleSessionManagerTest { | |||
|         val repeats = 1 | ||||
|         selectedTimer.set(FunctionalPomodoroTimer(studyTime, breakTime, repeats)) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  | @ -83,7 +81,7 @@ class InvisibleSessionManagerTest { | |||
|     fun InvisibleCustomTimerTest() = runTest { | ||||
|         selectedTimer.set(FunctionalCustomTimer(5)) | ||||
|         viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), SelectedTask(), LogServiceImpl()) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mediaPlayer) | ||||
|         InvisibleSessionManager.setParameters(viewModel, mock()) | ||||
| 
 | ||||
|         val test = launch { | ||||
|             InvisibleSessionManager.updateTimer() | ||||
|  |  | |||
		Reference in a new issue
	
	 Rune Dyselinck
						Rune Dyselinck