diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index c9ff4ce..80fd6a4 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -291,6 +291,19 @@ + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index b65e193..bd5886b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SessionScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SessionScreenTest.kt deleted file mode 100644 index dde1287..0000000 --- a/app/src/androidTest/java/be/ugent/sel/studeez/SessionScreenTest.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt index 22c2f3e..d4b5c68 100644 --- a/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt +++ b/app/src/androidTest/java/be/ugent/sel/studeez/SubjectScreenTest.kt @@ -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 ) ) ) diff --git a/app/src/androidTest/java/be/ugent/sel/studeez/TimerScreenTest.kt b/app/src/androidTest/java/be/ugent/sel/studeez/TimerScreenTest.kt deleted file mode 100644 index 40c5e4e..0000000 --- a/app/src/androidTest/java/be/ugent/sel/studeez/TimerScreenTest.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt index bc40ead..ea2b52d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FloatingActionButtonComposable.kt @@ -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 diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt new file mode 100644 index 0000000..1fbcfb2 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/FormComposable.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt new file mode 100644 index 0000000..39e7272 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ImageComposable.kt @@ -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) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt index aadcee3..47bdec5 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt @@ -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 = remember { mutableStateOf(true) }, + isFirst: MutableState = 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), diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt index 5db2af3..c6631ce 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/tasks/SubjectEntry.kt @@ -31,9 +31,13 @@ import be.ugent.sel.studeez.R.string as AppText fun SubjectEntry( subject: Subject, onViewSubject: () -> Unit, + getTaskCount: () -> Flow, + getCompletedTaskCount: () -> Flow, getStudyTime: () -> Flow, ) { 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() }, ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt new file mode 100644 index 0000000..87ce226 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/common/ext/ColorExt.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt index 74ebe9f..88c48c0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/task/Subject.kt @@ -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 { diff --git a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt index 765fbcd..f5237d6 100644 --- a/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt +++ b/app/src/main/java/be/ugent/sel/studeez/data/local/models/timer_functional/FunctionalPomodoroTimer.kt @@ -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-- diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt index bad8106..d887ef5 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/SubjectDAO.kt @@ -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 + fun getCompletedTaskCount(subject: Subject): Flow fun getStudyTime(subject: Subject): Flow suspend fun getSubject(subjectId: String): Subject? diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt index b023986..915a7f9 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FireBaseSubjectDAO.kt @@ -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 { + return taskDAO.getTasks(subject) + .map(List::count) + } + + override fun getCompletedTaskCount(subject: Subject): Flow { + return taskDAO.getTasks(subject) + .map { tasks -> tasks.count { it.completed && !it.archived } } } override fun getStudyTime(subject: Subject): Flow { diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt index 37085f1..0a0e8b4 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezNavGraph.kt @@ -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() ) } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt index 9051fa8..948c4c4 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/InvisibleSessionManager.kt @@ -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() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt index 084ff43..aeaf544 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/SessionRoute.kt @@ -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() } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt new file mode 100644 index 0000000..14fae19 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/SoundPlayer.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt deleted file mode 100644 index 08a8a72..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/AbstractSessionScreen.kt +++ /dev/null @@ -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" }, {}, {}, {})) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt deleted file mode 100644 index 9c59b46..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakSessionScreen.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt new file mode 100644 index 0000000..42ec4f7 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/BreakTimerScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt deleted file mode 100644 index 7fc60bc..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomSessionScreen.kt +++ /dev/null @@ -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() - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt new file mode 100644 index 0000000..a0c385c --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/CustomTimerSessionScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt deleted file mode 100644 index be67cff..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessSessionScreen.kt +++ /dev/null @@ -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() {} -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt new file mode 100644 index 0000000..4f1dbe3 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/EndlessTimerSessionScreenComposable.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt deleted file mode 100644 index 98b2d5e..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreen.kt +++ /dev/null @@ -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 { - 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) -} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt new file mode 100644 index 0000000..47ca52e --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/GetSessionScreenComposable.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt new file mode 100644 index 0000000..c94d2a5 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/SessionScreenComposable.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt new file mode 100644 index 0000000..2a29403 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session/sessionScreens/TimerComposable.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt index 2d06e0b..3a1e85f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapScreen.kt @@ -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, + ) }, + {}, + {}, + ) + ) +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt index c11e28f..bf11b93 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/session_recap/SessionRecapViewModel.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt index ab2cff4..852c0e5 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectScreen.kt @@ -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, + getCompletedTaskCount: (Subject) -> Flow, getStudyTime: (Subject) -> Flow, 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, ) } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt index c158529..19ba396 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/SubjectViewModel.kt @@ -30,6 +30,14 @@ class SubjectViewModel @Inject constructor( open(StudeezDestinations.ADD_SUBJECT_FORM) } + fun getTaskCount(subject: Subject): Flow { + return subjectDAO.getTaskCount(subject) + } + + fun getCompletedTaskCount(subject: Subject): Flow { + return subjectDAO.getCompletedTaskCount(subject) + } + fun getStudyTime(subject: Subject): Flow { return subjectDAO.getStudyTime(subject) } diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt index 19e6816..196ad3f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormScreen.kt @@ -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() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt index 9fdba01..10a18e8 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormUiState.kt @@ -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(), ) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt index 533123b..84162d0 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/subjects/form/SubjectFormViewModel.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt index 92302ea..79c744d 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/tasks/form/TaskFormScreen.kt @@ -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, ) { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt deleted file mode 100644 index 0afe2a0..0000000 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormRoute.kt +++ /dev/null @@ -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 = {} - ) -} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt new file mode 100644 index 0000000..c69e929 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormScreen.kt @@ -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) + } +} diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt index 8a0a4d4..c34cd06 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/TimerFormViewModel.kt @@ -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() diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt index 5f4a17b..9560dd1 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/AbstractTimerFormScreen.kt @@ -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. diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt index 12d07a4..c87bd7b 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/form_screens/BreakTimerFormScreen.kt @@ -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() { diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt index 5e251f2..3663e4a 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/timer_form/timer_type_select/TimerTypeSelectScreen.kt @@ -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) } } diff --git a/app/src/main/res/drawable/mood_1.xml b/app/src/main/res/drawable/mood_1.xml new file mode 100644 index 0000000..bf009f2 --- /dev/null +++ b/app/src/main/res/drawable/mood_1.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/mood_2.xml b/app/src/main/res/drawable/mood_2.xml new file mode 100644 index 0000000..0fd3daa --- /dev/null +++ b/app/src/main/res/drawable/mood_2.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70c4558..a5f350e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,6 +46,7 @@ Delete Subject Delete Task View + Regenerate Color 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. @@ -69,8 +70,15 @@ Timers + Delete Timer Edit Add timer + + Name should not be blank + Description should not be blank + Fill out all the fields correctly! + + Select time Focus! @@ -149,4 +157,11 @@ Break Time Number of Repeats + + "Congratulations! You studied: %s" + How did it go? + Good + Bad + + diff --git a/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt b/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt index 06b1d42..8f1cca7 100644 --- a/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt +++ b/app/src/test/java/be/ugent/sel/studeez/timer_functional/InvisibleSessionManagerTest.kt @@ -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()