commit
						d9d83569db
					
				
					 26 changed files with 549 additions and 51 deletions
				
			
		|  | @ -3,10 +3,13 @@ package be.ugent.sel.studeez.common.composable | |||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| 
 | ||||
| @Composable | ||||
|  | @ -24,3 +27,13 @@ fun Headline( | |||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun DateText(date: String) { | ||||
|     Text( | ||||
|         text = date, | ||||
|         fontWeight = FontWeight.Bold, | ||||
|         fontSize = 20.sp, | ||||
|         modifier = Modifier.padding(horizontal = 10.dp) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,160 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import be.ugent.sel.studeez.common.composable.BasicTextButton | ||||
| import be.ugent.sel.studeez.common.composable.DateText | ||||
| import be.ugent.sel.studeez.common.composable.Headline | ||||
| import be.ugent.sel.studeez.common.ext.textButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.R.string as AppText | ||||
| 
 | ||||
| @Composable | ||||
| fun Feed( | ||||
|     uiState: FeedUiState, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit | ||||
| ) { | ||||
|     when (uiState) { | ||||
|         FeedUiState.Loading -> LoadingFeed() | ||||
|         is FeedUiState.Succes -> LoadedFeed( | ||||
|             uiState = uiState, | ||||
|             continueTask = continueTask, | ||||
|             onEmptyFeedHelp = onEmptyFeedHelp | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun LoadedFeed( | ||||
|     uiState: FeedUiState.Succes, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit, | ||||
| ) { | ||||
|     if (uiState.feedEntries.isEmpty()) EmptyFeed(onEmptyFeedHelp) | ||||
|     else FeedWithElements(uiState = uiState, continueTask = continueTask) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun LoadingFeed() { | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .fillMaxHeight(), | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|         horizontalAlignment = Alignment.CenterHorizontally | ||||
|     ) { | ||||
|         CircularProgressIndicator(color = MaterialTheme.colors.onBackground) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun FeedWithElements( | ||||
|     uiState: FeedUiState.Succes, | ||||
|     continueTask: (String, String) -> Unit, | ||||
| ) { | ||||
|     val feedEntries = uiState.feedEntries | ||||
|     LazyColumn { | ||||
|         items(feedEntries.toList()) { (date, feedEntries) -> | ||||
|             Row( | ||||
|                 horizontalArrangement = Arrangement.SpaceBetween, | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxWidth() | ||||
|                     .padding(10.dp), | ||||
|                 verticalAlignment = Alignment.CenterVertically | ||||
|             ) { | ||||
|                 val totalDayStudyTime: Int = feedEntries.sumOf { it.totalStudyTime } | ||||
|                 DateText(date = date) | ||||
|                 Text( | ||||
|                     text = "${HoursMinutesSeconds(totalDayStudyTime)}", | ||||
|                     fontSize = 15.sp, | ||||
|                     fontWeight = FontWeight.Bold | ||||
|                 ) | ||||
|             } | ||||
|             feedEntries.forEach { feedEntry -> | ||||
|                 FeedEntry(feedEntry = feedEntry) { | ||||
|                     continueTask(feedEntry.subjectId, feedEntry.taskId) | ||||
|                 } | ||||
|             } | ||||
|             Spacer(modifier = Modifier.height(20.dp)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun EmptyFeed(onEmptyFeedHelp: () -> Unit) { | ||||
|     Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { | ||||
|         Column( | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|             modifier = Modifier.fillMaxWidth() | ||||
|         ) { | ||||
|             Headline(text = stringResource(id = AppText.your_feed)) | ||||
| 
 | ||||
|             BasicTextButton( | ||||
|                 AppText.empty_feed_help_text, | ||||
|                 Modifier.textButton(), | ||||
|                 action = onEmptyFeedHelp, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedLoadingPreview() { | ||||
|     Feed( | ||||
|         uiState = FeedUiState.Loading, | ||||
|         continueTask = { _, _ -> run {} }, {} | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedPreview() { | ||||
|     Feed( | ||||
|         uiState = FeedUiState.Succes( | ||||
|             mapOf( | ||||
|                 "08 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 600, | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 20, | ||||
|                     ), | ||||
|                 ), | ||||
|                 "09 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFD1200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                 ) | ||||
|             ) | ||||
|         ), | ||||
|         continueTask = { _, _ -> run {} }, {} | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,96 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.* | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.material.Card | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| 
 | ||||
| @Composable | ||||
| fun FeedEntry( | ||||
|     feedEntry: FeedEntry, | ||||
|     continueWithTask: () -> Unit, | ||||
| ) { | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(horizontal = 10.dp, vertical = 5.dp), | ||||
|     ) { | ||||
|         Row( | ||||
|             horizontalArrangement = Arrangement.SpaceBetween, | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             Row( | ||||
|                 horizontalArrangement = Arrangement.spacedBy(8.dp), | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = 10.dp) | ||||
|                     .weight(11f) | ||||
|             ) { | ||||
|                 Box( | ||||
|                     modifier = Modifier | ||||
|                         .size(20.dp) | ||||
|                         .clip(CircleShape) | ||||
|                         .background(Color(feedEntry.argb_color)), | ||||
|                 ) | ||||
|                 Row( | ||||
|                     modifier = Modifier.fillMaxWidth(), | ||||
|                     horizontalArrangement = Arrangement.SpaceBetween, | ||||
|                     verticalAlignment = Alignment.CenterVertically, | ||||
|                 ) { | ||||
|                     Column( | ||||
|                         verticalArrangement = Arrangement.spacedBy(0.dp) | ||||
|                     ) { | ||||
|                         Text( | ||||
|                             text = feedEntry.subJectName, | ||||
|                             fontWeight = FontWeight.Bold, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             maxLines = 1, | ||||
|                         ) | ||||
|                         Text( | ||||
|                             text = feedEntry.taskName, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             maxLines = 1, | ||||
|                         ) | ||||
|                     } | ||||
|                     Text(text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString()) | ||||
|                 } | ||||
|             } | ||||
|             StealthButton( | ||||
|                 text = R.string.continue_task, | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = 10.dp, end = 5.dp) | ||||
|                     .weight(6f) | ||||
|             ) { | ||||
|                 continueWithTask() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview | ||||
| @Composable | ||||
| fun FeedEntryPreview() { | ||||
|     FeedEntry( | ||||
|         continueWithTask = {}, | ||||
|         feedEntry = FeedEntry( | ||||
|             argb_color = 0xFFFFD200, | ||||
|             subJectName = "Test Subject", | ||||
|             taskName = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkk", | ||||
|             totalStudyTime = 20, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| 
 | ||||
| sealed interface FeedUiState { | ||||
|     object Loading : FeedUiState | ||||
|     data class Succes(val feedEntries: Map<String, List<FeedEntry>>) : FeedUiState | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| package be.ugent.sel.studeez.common.composable.feed | ||||
| 
 | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.domain.FeedDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.flow.* | ||||
| import kotlinx.coroutines.launch | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class FeedViewModel @Inject constructor( | ||||
|     feedDAO: FeedDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|     private val selectedTask: SelectedTask, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     val uiState: StateFlow<FeedUiState> = feedDAO.getFeedEntries() | ||||
|         .map { FeedUiState.Succes(it) } | ||||
|         .stateIn( | ||||
|             scope = viewModelScope, | ||||
|             initialValue = FeedUiState.Loading, | ||||
|             started = SharingStarted.Eagerly, | ||||
|         ) | ||||
| 
 | ||||
|     fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) { | ||||
|         viewModelScope.launch { | ||||
|             val task = taskDAO.getTask(subjectId, taskId) | ||||
|             selectedTask.set(task) | ||||
|             open(StudeezDestinations.TIMER_SELECTION_SCREEN) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun onEmptyFeedHelp(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.ADD_SUBJECT_FORM) | ||||
|     } | ||||
| } | ||||
|  | @ -32,6 +32,7 @@ fun TaskEntry( | |||
|     task: Task, | ||||
|     onCheckTask: (Boolean) -> Unit, | ||||
|     onDeleteTask: () -> Unit, | ||||
|     onStartTask: () -> Unit | ||||
| ) { | ||||
|     Card( | ||||
|         modifier = Modifier | ||||
|  | @ -95,6 +96,7 @@ fun TaskEntry( | |||
|                         modifier = Modifier | ||||
|                             .padding(end = 5.dp), | ||||
|                     ) { | ||||
|                         onStartTask() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | @ -110,7 +112,7 @@ fun TaskEntryPreview() { | |||
|             name = "Test Task", | ||||
|             completed = false, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {} | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -122,7 +124,7 @@ fun CompletedTaskEntryPreview() { | |||
|             name = "Test Task", | ||||
|             completed = true, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {}, | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -134,6 +136,6 @@ fun OverflowTaskEntryPreview() { | |||
|             name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk", | ||||
|             completed = false, | ||||
|         ), | ||||
|         {}, {}, | ||||
|         {}, {}, {} | ||||
|     ) | ||||
| } | ||||
|  | @ -0,0 +1,13 @@ | |||
| package be.ugent.sel.studeez.data.local.models | ||||
| 
 | ||||
| import com.google.firebase.Timestamp | ||||
| 
 | ||||
| data class FeedEntry( | ||||
|     val argb_color: Long = 0, | ||||
|     val subJectName: String = "", | ||||
|     val taskName: String = "", | ||||
|     val taskId: String = "", // Name of task is not unique | ||||
|     val subjectId: String = "", | ||||
|     val totalStudyTime: Int = 0, | ||||
|     val endTime: Timestamp = Timestamp(0, 0) | ||||
| ) | ||||
|  | @ -6,5 +6,7 @@ import com.google.firebase.firestore.DocumentId | |||
| data class SessionReport( | ||||
|     @DocumentId val id: String = "", | ||||
|     val studyTime: Int = 0, | ||||
|     val endTime: Timestamp = Timestamp(0, 0) | ||||
|     val endTime: Timestamp = Timestamp(0, 0), | ||||
|     val taskId: String = "", | ||||
|     val subjectId: String = "" | ||||
| ) | ||||
|  | @ -2,6 +2,7 @@ package be.ugent.sel.studeez.data.local.models.timer_functional | |||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import com.google.firebase.Timestamp | ||||
| import com.google.firebase.firestore.DocumentReference | ||||
| 
 | ||||
| abstract class FunctionalTimer(initialValue: Int) { | ||||
|     var time: Time = Time(initialValue) | ||||
|  | @ -17,10 +18,12 @@ abstract class FunctionalTimer(initialValue: Int) { | |||
| 
 | ||||
|     abstract fun hasCurrentCountdownEnded(): Boolean | ||||
| 
 | ||||
|     fun getSessionReport(): SessionReport { | ||||
|     fun getSessionReport(subjectId: String, taskId: String): SessionReport { | ||||
|         return SessionReport( | ||||
|             studyTime = totalStudyTime, | ||||
|             endTime = Timestamp.now() | ||||
|             endTime = Timestamp.now(), | ||||
|             taskId = taskId, | ||||
|             subjectId = subjectId | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,4 +33,7 @@ abstract class DatabaseModule { | |||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideTaskDAO(impl: FireBaseTaskDAO): TaskDAO | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO | ||||
| } | ||||
							
								
								
									
										10
									
								
								app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/java/be/ugent/sel/studeez/domain/FeedDAO.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| package be.ugent.sel.studeez.domain | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| 
 | ||||
| interface FeedDAO { | ||||
| 
 | ||||
|     fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> | ||||
| 
 | ||||
| } | ||||
|  | @ -12,4 +12,5 @@ interface SubjectDAO { | |||
|     fun deleteSubject(oldSubject: Subject) | ||||
| 
 | ||||
|     fun updateSubject(newSubject: Subject) | ||||
|     suspend fun getSubject(subjectId: String): Subject? | ||||
| } | ||||
|  | @ -15,4 +15,6 @@ interface TaskDAO { | |||
|     fun deleteTask(oldTask: Task) | ||||
| 
 | ||||
|     fun toggleTaskCompleted(task: Task, completed: Boolean) | ||||
| 
 | ||||
|     suspend fun getTask(subjectId: String, taskId: String): Task | ||||
| } | ||||
|  | @ -1,13 +1,16 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.SubjectDAO | ||||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import com.google.firebase.firestore.ktx.toObject | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FireBaseSubjectDAO @Inject constructor( | ||||
|  | @ -20,6 +23,10 @@ class FireBaseSubjectDAO @Inject constructor( | |||
|             .map { it.toObjects(Subject::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getSubject(subjectId: String): Subject? { | ||||
|         return currentUserSubjectsCollection().document(subjectId).get().await().toObject() | ||||
|     } | ||||
| 
 | ||||
|     override fun saveSubject(newSubject: Subject) { | ||||
|         currentUserSubjectsCollection().add(newSubject) | ||||
|     } | ||||
|  |  | |||
|  | @ -8,8 +8,11 @@ import be.ugent.sel.studeez.domain.TaskDAO | |||
| import com.google.firebase.firestore.CollectionReference | ||||
| import com.google.firebase.firestore.FirebaseFirestore | ||||
| import com.google.firebase.firestore.ktx.snapshots | ||||
| import com.google.firebase.firestore.ktx.toObject | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.tasks.await | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FireBaseTaskDAO @Inject constructor( | ||||
|  | @ -22,6 +25,10 @@ class FireBaseTaskDAO @Inject constructor( | |||
|             .map { it.toObjects(Task::class.java) } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getTask(subjectId: String, taskId: String): Task { | ||||
|         return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!! | ||||
|     } | ||||
| 
 | ||||
|     override fun saveTask(newTask: Task) { | ||||
|         selectedSubjectTasksCollection(newTask.subjectId).add(newTask) | ||||
|     } | ||||
|  |  | |||
|  | @ -0,0 +1,78 @@ | |||
| package be.ugent.sel.studeez.domain.implementation | ||||
| 
 | ||||
| import android.icu.text.DateFormat | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.data.local.models.SessionReport | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.domain.FeedDAO | ||||
| import be.ugent.sel.studeez.domain.SessionDAO | ||||
| import be.ugent.sel.studeez.domain.TaskDAO | ||||
| import com.google.firebase.Timestamp | ||||
| import kotlinx.coroutines.flow.* | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class FirebaseFeedDAO @Inject constructor( | ||||
|     private val sessionDAO: SessionDAO, | ||||
|     private val taskDAO: TaskDAO, | ||||
|     private val subjectDAO: FireBaseSubjectDAO | ||||
| ) : FeedDAO { | ||||
| 
 | ||||
|     /** | ||||
|      *  Return a map as with key the day and value a list of feedentries for that day. | ||||
|      */ | ||||
|     override fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> { | ||||
|         return sessionDAO.getSessions().map {sessionReports -> | ||||
|             sessionReports | ||||
|                 .map { sessionReport ->  sessionToFeedEntry(sessionReport) } | ||||
|                 .sortedByDescending { it.endTime } | ||||
|                 .groupBy { getFormattedTime(it) } | ||||
|                 .mapValues { (_, entries) -> | ||||
|                     entries | ||||
|                     .groupBy { it.taskId } | ||||
|                     .map { fuseFeedEntries(it.component2()) } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getFormattedTime(entry: FeedEntry): String { | ||||
|         return DateFormat.getDateInstance().format(entry.endTime.toDate()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Givin a list of entries referencing the same task, in the same day, fuse them into one | ||||
|      * feed-entry by adding the studytime and keeping the most recent end-timestamp | ||||
|      */ | ||||
|     private fun fuseFeedEntries(entries: List<FeedEntry>): FeedEntry = | ||||
|         entries.drop(1).fold(entries[0]) { accEntry, newEntry -> | ||||
|             accEntry.copy( | ||||
|                 totalStudyTime = accEntry.totalStudyTime + newEntry.totalStudyTime, | ||||
|                 endTime = getMostRecent(accEntry.endTime, newEntry.endTime) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     private fun getMostRecent(t1: Timestamp, t2: Timestamp): Timestamp { | ||||
|         return if (t1 < t2) t2 else t1 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names | ||||
|      */ | ||||
|     private suspend fun sessionToFeedEntry(sessionReport: SessionReport): FeedEntry { | ||||
|         val subjectId: String = sessionReport.subjectId | ||||
|         val taskId: String = sessionReport.taskId | ||||
| 
 | ||||
|         val task: Task = taskDAO.getTask(subjectId, taskId) | ||||
|         val subject: Subject = subjectDAO.getSubject(subjectId)!! | ||||
| 
 | ||||
|         return FeedEntry( | ||||
|             argb_color = subject.argb_color, | ||||
|             subJectName = subject.name, | ||||
|             taskName = task.name, | ||||
|             taskId = task.id, | ||||
|             subjectId = subject.id, | ||||
|             totalStudyTime = sessionReport.studyTime, | ||||
|             endTime = sessionReport.endTime | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -65,9 +65,9 @@ fun StudeezNavGraph( | |||
|         composable(StudeezDestinations.HOME_SCREEN) { | ||||
|             HomeRoute( | ||||
|                 open, | ||||
|                 viewModel = hiltViewModel(), | ||||
|                 drawerActions = drawerActions, | ||||
|                 navigationBarActions = navigationBarActions | ||||
|                 navigationBarActions = navigationBarActions, | ||||
|                 feedViewModel = hiltViewModel(), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,35 +5,45 @@ import androidx.compose.material.IconButton | |||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Person | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.BasicButton | ||||
| import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.drawer.DrawerActions | ||||
| import be.ugent.sel.studeez.common.composable.feed.Feed | ||||
| import be.ugent.sel.studeez.common.composable.feed.FeedUiState | ||||
| import be.ugent.sel.studeez.common.composable.feed.FeedViewModel | ||||
| import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions | ||||
| import be.ugent.sel.studeez.common.ext.basicButton | ||||
| import be.ugent.sel.studeez.data.local.models.FeedEntry | ||||
| import be.ugent.sel.studeez.resources | ||||
| 
 | ||||
| @Composable | ||||
| fun HomeRoute( | ||||
|     open: (String) -> Unit, | ||||
|     viewModel: HomeViewModel, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedViewModel: FeedViewModel, | ||||
| ) { | ||||
|     val feedUiState by feedViewModel.uiState.collectAsState() | ||||
|     HomeScreen( | ||||
|         onStartSessionClick = { viewModel.onStartSessionClick(open) }, | ||||
|         drawerActions = drawerActions, | ||||
|         open = open, | ||||
|         navigationBarActions = navigationBarActions, | ||||
|         feedUiState = feedUiState, | ||||
|         continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) }, | ||||
|         onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun HomeScreen( | ||||
|     onStartSessionClick: () -> Unit, | ||||
|     open: (String) -> Unit, | ||||
|     drawerActions: DrawerActions, | ||||
|     navigationBarActions: NavigationBarActions | ||||
|     navigationBarActions: NavigationBarActions, | ||||
|     feedUiState: FeedUiState, | ||||
|     continueTask: (String, String) -> Unit, | ||||
|     onEmptyFeedHelp: () -> Unit, | ||||
| ) { | ||||
|     PrimaryScreenTemplate( | ||||
|         title = resources().getString(R.string.home), | ||||
|  | @ -41,9 +51,7 @@ fun HomeScreen( | |||
|         navigationBarActions = navigationBarActions, | ||||
|         // TODO barAction = { FriendsAction() } | ||||
|     ) { | ||||
|         BasicButton(R.string.start_session, Modifier.basicButton()) { | ||||
|             onStartSessionClick() | ||||
|         } | ||||
|         Feed(feedUiState, continueTask, onEmptyFeedHelp) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -61,8 +69,40 @@ fun FriendsAction() { | |||
| @Composable | ||||
| fun HomeScreenPreview() { | ||||
|     HomeScreen( | ||||
|         onStartSessionClick = {}, | ||||
|         drawerActions = DrawerActions({}, {}, {}, {}, {}), | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}) | ||||
|         navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}), | ||||
|         open = {}, | ||||
|         feedUiState = FeedUiState.Succes( | ||||
|             mapOf( | ||||
|                 "08 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFABD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 600, | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFFD200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                         totalStudyTime = 20, | ||||
|                     ), | ||||
|                 ), | ||||
|                 "09 May 2023" to listOf( | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFD1200, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                     FeedEntry( | ||||
|                         argb_color = 0xFFFF5C89, | ||||
|                         subJectName = "Test Subject", | ||||
|                         taskName = "Test Task", | ||||
|                     ), | ||||
|                 ) | ||||
|             ) | ||||
|         ), | ||||
|         continueTask = { _, _ -> run {} }, | ||||
|         onEmptyFeedHelp = {} | ||||
|     ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,19 +0,0 @@ | |||
| package be.ugent.sel.studeez.screens.home | ||||
| 
 | ||||
| import be.ugent.sel.studeez.domain.AccountDAO | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
| import be.ugent.sel.studeez.screens.StudeezViewModel | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class HomeViewModel @Inject constructor( | ||||
|     private val accountDAO: AccountDAO, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     fun onStartSessionClick(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.TIMER_SELECTION_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,9 @@ | |||
| package be.ugent.sel.studeez.screens.session | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.SessionReportState | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.navigation.StudeezDestinations | ||||
|  | @ -13,21 +15,22 @@ import javax.inject.Inject | |||
| class SessionViewModel @Inject constructor( | ||||
|     private val selectedTimerState: SelectedTimerState, | ||||
|     private val sessionReportState: SessionReportState, | ||||
|     private val selectedTask: SelectedTask, | ||||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     private val task : String = "No task selected" // placeholder for tasks implementation | ||||
|     private val task : Task = selectedTask() | ||||
| 
 | ||||
|     fun getTimer() : FunctionalTimer { | ||||
|         return selectedTimerState.selectedTimer!! | ||||
|     } | ||||
| 
 | ||||
|     fun getTask(): String { | ||||
|         return task | ||||
|         return task.name | ||||
|     } | ||||
| 
 | ||||
|     fun endSession(openAndPopUp: (String, String) -> Unit) { | ||||
|         sessionReportState.sessionReport = getTimer().getSessionReport() | ||||
|         sessionReportState.sessionReport = getTimer().getSessionReport(task.subjectId, task.id) | ||||
|         openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -30,6 +30,7 @@ data class TaskActions( | |||
|     val deleteTask: (Task) -> Unit, | ||||
|     val onCheckTask: (Task, Boolean) -> Unit, | ||||
|     val editSubject: () -> Unit, | ||||
|     val startTask: (Task) -> Unit | ||||
| ) | ||||
| 
 | ||||
| fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskActions { | ||||
|  | @ -39,7 +40,8 @@ fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskAction | |||
|         getSubject = viewModel::getSelectedSubject, | ||||
|         deleteTask = viewModel::deleteTask, | ||||
|         onCheckTask = { task, isChecked -> viewModel.toggleTaskCompleted(task, isChecked) }, | ||||
|         editSubject = { viewModel.editSubject(open) } | ||||
|         editSubject = { viewModel.editSubject(open) }, | ||||
|         startTask = { task -> viewModel.startTask(task, open) } | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | @ -75,6 +77,7 @@ fun TaskScreen( | |||
|                         task = it, | ||||
|                         onCheckTask = { isChecked -> taskActions.onCheckTask(it, isChecked) }, | ||||
|                         onDeleteTask = { taskActions.deleteTask(it) }, | ||||
|                         onStartTask = { taskActions.startTask(it) } | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|  | @ -108,6 +111,7 @@ fun TaskScreenPreview() { | |||
|             {}, | ||||
|             { _, _ -> run {} }, | ||||
|             {}, | ||||
|             {} | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| package be.ugent.sel.studeez.screens.tasks | ||||
| 
 | ||||
| import be.ugent.sel.studeez.data.SelectedSubject | ||||
| import be.ugent.sel.studeez.data.SelectedTask | ||||
| import be.ugent.sel.studeez.data.local.models.task.Subject | ||||
| import be.ugent.sel.studeez.data.local.models.task.Task | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
|  | @ -17,6 +18,7 @@ class TaskViewModel @Inject constructor( | |||
|     private val taskDAO: TaskDAO, | ||||
|     private val subjectDAO: SubjectDAO, | ||||
|     private val selectedSubject: SelectedSubject, | ||||
|     private val selectedTask: SelectedTask, | ||||
|     logService: LogService, | ||||
| ) : StudeezViewModel(logService) { | ||||
|     fun addTask(open: (String) -> Unit) { | ||||
|  | @ -47,4 +49,9 @@ class TaskViewModel @Inject constructor( | |||
|     fun editSubject(open: (String) -> Unit) { | ||||
|         open(StudeezDestinations.EDIT_SUBJECT_FORM) | ||||
|     } | ||||
| 
 | ||||
|     fun startTask(task: Task, open: (String) -> Unit) { | ||||
|         selectedTask.set(task) | ||||
|         open(StudeezDestinations.TIMER_SELECTION_SCREEN) | ||||
|     } | ||||
| } | ||||
|  | @ -82,12 +82,13 @@ fun TimerOverviewScreen( | |||
|                 items(timers.value) { timerInfo -> | ||||
|                     TimerEntry( | ||||
|                         timerInfo = timerInfo, | ||||
|                     ) { | ||||
|                         StealthButton( | ||||
|                             text = R.string.edit, | ||||
|                             onClick = { timerOverviewActions.onEditClick(timerInfo) } | ||||
|                         ) | ||||
|                     } | ||||
|                         rightButton = { | ||||
|                             StealthButton( | ||||
|                                 text = R.string.edit, | ||||
|                                 onClick = { timerOverviewActions.onEditClick(timerInfo) } | ||||
|                             ) | ||||
|                         } | ||||
|                     ) | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,10 +1,13 @@ | |||
| package be.ugent.sel.studeez.screens.timer_selection | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import be.ugent.sel.studeez.R | ||||
| import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate | ||||
| import be.ugent.sel.studeez.common.composable.StealthButton | ||||
|  | @ -99,7 +102,10 @@ fun CustomTimerEntry( | |||
|             ) | ||||
|         }, | ||||
|         rightButton = { | ||||
|             TimePickerButton(initialSeconds = hms.getTotalSeconds()) { chosenTime -> | ||||
|             TimePickerButton( | ||||
|                 initialSeconds = hms.getTotalSeconds(), | ||||
|                 modifier = Modifier.padding(horizontal = 5.dp) | ||||
|             ) { chosenTime -> | ||||
|                 timerInfo.studyTime = chosenTime | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import androidx.compose.runtime.getValue | |||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import be.ugent.sel.studeez.data.SelectedTimerState | ||||
| import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds | ||||
| import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo | ||||
| import be.ugent.sel.studeez.domain.LogService | ||||
| import be.ugent.sel.studeez.domain.TimerDAO | ||||
|  | @ -21,7 +22,9 @@ class TimerSelectionViewModel @Inject constructor( | |||
|     logService: LogService | ||||
| ) : StudeezViewModel(logService) { | ||||
| 
 | ||||
|     var customTimerStudyTime: MutableState<Int> = mutableStateOf(0) | ||||
|     var customTimerStudyTime: MutableState<Int> = mutableStateOf( | ||||
|         HoursMinutesSeconds(1, 0, 0).getTotalSeconds() | ||||
|     ) | ||||
| 
 | ||||
|     fun getAllTimers() : Flow<List<TimerInfo>> { | ||||
|         return timerDAO.getAllTimers() | ||||
|  |  | |||
|  | @ -29,6 +29,11 @@ | |||
|     <string name="home">Home</string> | ||||
|     <string name="start_session">Start session</string> | ||||
| 
 | ||||
|     <!-- Feed--> | ||||
|     <string name="continue_task">Continue</string> | ||||
|     <string name="your_feed">This is your feed</string> | ||||
|     <string name="empty_feed_help_text">Create you first subject and tasks to get started</string> | ||||
| 
 | ||||
|     <!-- Tasks --> | ||||
|     <string name="tasks">Tasks</string> | ||||
|     <string name="task">Task</string> | ||||
|  |  | |||
		Reference in a new issue
	
	 brreynie
						brreynie