Merge branch 'development' into sessionscreen_ui

This commit is contained in:
rdyselin 2023-04-18 15:41:04 +02:00 committed by GitHub Enterprise
commit 9d2c53d4e6
26 changed files with 477 additions and 79 deletions

View file

@ -26,6 +26,7 @@ import be.ugent.sel.studeez.screens.profile.ProfileScreen
import be.ugent.sel.studeez.screens.sign_up.SignUpScreen
import be.ugent.sel.studeez.screens.splash.SplashScreen
import be.ugent.sel.studeez.screens.timer_overview.TimerOverviewScreen
import be.ugent.sel.studeez.screens.timer_selection.TimerSelectionScreen
import be.ugent.sel.studeez.ui.theme.StudeezTheme
import kotlinx.coroutines.CoroutineScope
@ -130,4 +131,8 @@ fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) {
composable(StudeezDestinations.EDIT_PROFILE_SCREEN) {
EditProfileScreen(goBack, openAndPopUp)
}
composable(StudeezDestinations.TIMER_SELECTION_SCREEN) {
TimerSelectionScreen(open, openAndPopUp)
}
}

View file

@ -0,0 +1,14 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the SelectedTimer from the selection screen to the session screen.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedTimerState @Inject constructor(){
var selectedTimer: FunctionalTimer? = null
}

View file

@ -1,3 +1,3 @@
package be.ugent.sel.studeez.data.local.models
data class User(val id: String = "", val isAnonymous: Boolean = true)
data class User(val id: String = "")

View file

@ -3,14 +3,14 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
class FunctionalCustomTimer(studyTime: Int) : FunctionalTimer(studyTime) {
override fun tick() {
if (time.getTime() == 0) {
view = "Done!"
if (time.time == 0) {
view = StudyState.DONE
} else {
time.minOne()
}
}
override fun hasEnded(): Boolean {
return time.getTime() == 0
return time.time == 0
}
}

View file

@ -5,23 +5,23 @@ class FunctionalPomodoroTimer(
private var breakTime: Int, repeats: Int
) : FunctionalTimer(studyTime) {
private var breaksRemaining = repeats
private var isInBreak = false
var breaksRemaining = repeats
var isInBreak = false
override fun tick() {
if (time.getTime() == 0 && breaksRemaining == 0){
view = "Done!"
if (time.time == 0 && breaksRemaining == 0) {
view = StudyState.DONE
return
}
if (time.getTime() == 0) {
if (time.time == 0) {
if (isInBreak) {
breaksRemaining--
view = "Focus! ($breaksRemaining breaks remaining)"
time.setTime(studyTime)
view = StudyState.FOCUS_REMAINING
time.time = studyTime
} else {
view = "Take a break!"
time.setTime(breakTime)
view = StudyState.BREAK
time.time = breakTime
}
isInBreak = !isInBreak
}
@ -29,6 +29,6 @@ class FunctionalPomodoroTimer(
}
override fun hasEnded(): Boolean {
return breaksRemaining == 0 && time.getTime() == 0
return breaksRemaining == 0 && time.time == 0
}
}

View file

@ -1,17 +1,13 @@
package be.ugent.sel.studeez.data.local.models.timer_functional
abstract class FunctionalTimer(initialValue: Int) {
protected val time: Time = Time(initialValue)
protected var view: String = "Focus"
val time: Time = Time(initialValue)
var view: StudyState = StudyState.FOCUS
fun getHoursMinutesSeconds(): HoursMinutesSeconds {
return time.getAsHMS()
}
fun getViewString(): String {
return view
}
abstract fun tick()
abstract fun hasEnded(): Boolean
@ -19,4 +15,9 @@ abstract class FunctionalTimer(initialValue: Int) {
fun hasCurrentCountdownEnded(): Boolean {
return time.getTime() == 0
}
enum class StudyState {
FOCUS, DONE, BREAK, FOCUS_REMAINING
}
}

View file

@ -2,7 +2,7 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
class Time(initialTime: Int) {
private var time = initialTime
var time = initialTime
fun minOne() {
time--
@ -12,14 +12,6 @@ class Time(initialTime: Int) {
time++
}
fun setTime(newTime: Int) {
time = newTime
}
fun getTime(): Int {
return time
}
fun getAsHMS(): HoursMinutesSeconds {
val hours: Int = time / (60 * 60)
val minutes: Int = (time / (60)) % 60

View file

@ -3,7 +3,7 @@ package be.ugent.sel.studeez.data.local.models.timer_info
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
class BreakTimerInfo(
class PomodoroTimerInfo(
name: String,
description: String,
private val studyTime: Int,

View file

@ -39,7 +39,7 @@ class FirebaseAccountDAO @Inject constructor(
get() = callbackFlow {
val listener =
FirebaseAuth.AuthStateListener { auth ->
this.trySend(auth.currentUser?.let { User(it.uid, it.isAnonymous) } ?: User())
this.trySend(auth.currentUser?.let { User(it.uid) } ?: User())
}
auth.addAuthStateListener(listener)
awaitClose { auth.removeAuthStateListener(listener) }

View file

@ -30,7 +30,7 @@ class ToTimerConverter {
it.studyTime,
it.id
) },
TimerType.BREAK to TimerFactory { BreakTimerInfo(
TimerType.BREAK to TimerFactory { PomodoroTimerInfo(
it.name,
it.description,
it.studyTime,

View file

@ -7,6 +7,7 @@ object StudeezDestinations {
const val HOME_SCREEN = "home"
const val TIMER_OVERVIEW_SCREEN = "timer_overview"
const val TIMER_SELECTION_SCREEN = "timer_selection"
const val SESSION_SCREEN = "session"
// const val TASKS_SCREEN = "tasks"
// const val SESSIONS_SCREEN = "sessions"

View file

@ -15,14 +15,7 @@ class HomeViewModel @Inject constructor(
logService: LogService
) : StudeezViewModel(logService) {
fun onStartSessionClick(openAndPopUp: (String) -> Unit) {
openAndPopUp(StudeezDestinations.SESSION_SCREEN)
}
fun onLogoutClick(openAndPopup: (String, String) -> Unit) {
launchCatching {
accountDAO.signOut()
openAndPopup(LOGIN_SCREEN, HOME_SCREEN)
}
fun onStartSessionClick(open: (String) -> Unit) {
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
}
}

View file

@ -14,16 +14,28 @@ import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.navigation.StudeezDestinations
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer.StudyState
import be.ugent.sel.studeez.resources
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
var timerEnd = false
@ -31,7 +43,6 @@ var timerEnd = false
fun SessionScreen(
open: (String) -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: SessionViewModel = hiltViewModel()
) {
val context = LocalContext.current
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
@ -86,9 +97,9 @@ fun SessionScreen(
private fun Timer(viewModel: SessionViewModel = hiltViewModel(), mediaplayer: MediaPlayer) {
var tikker by remember { mutableStateOf(false) }
LaunchedEffect(tikker) {
delay(1000)
delay(1.seconds)
viewModel.getTimer().tick()
tikker = !tikker
ticker = !ticker
}
if (viewModel.getTimer().hasCurrentCountdownEnded() && !viewModel.getTimer().hasEnded()) {
@ -111,8 +122,18 @@ private fun Timer(viewModel: SessionViewModel = hiltViewModel(), mediaplayer: Me
fontWeight = FontWeight.Bold,
fontSize = 40.sp,
)
val stateString: String = when (viewModel.getTimer().view) {
StudyState.DONE -> resources().getString(R.string.state_done)
StudyState.FOCUS -> resources().getString(R.string.state_focus)
StudyState.BREAK -> resources().getString(R.string.state_take_a_break)
StudyState.FOCUS_REMAINING ->
(viewModel.getTimer() as FunctionalPomodoroTimer?)?.breaksRemaining?.let {
resources().getQuantityString(R.plurals.state_focus_remaining, it, it)
}.toString()
}
Text(
text = viewModel.getTimer().getViewString(),
text = stateString,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
@ -142,4 +163,6 @@ private fun Timer(viewModel: SessionViewModel = hiltViewModel(), mediaplayer: Me
}
}
}
}

View file

@ -1,14 +1,15 @@
package be.ugent.sel.studeez.screens.session
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.screens.StudeezViewModel
import be.ugent.sel.studeez.data.SelectedTimerState
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SessionViewModel @Inject constructor(
private val selectedTimerState: SelectedTimerState,
logService: LogService
) : StudeezViewModel(logService) {
@ -16,7 +17,7 @@ class SessionViewModel @Inject constructor(
private val task : String = "No task selected" // placeholder for tasks implementation
fun getTimer() : FunctionalTimer {
return timer
return selectedTimerState.selectedTimer!!
}
fun getTask(): String {

View file

@ -1,5 +1,6 @@
package be.ugent.sel.studeez.screens.timer_overview
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -46,12 +47,12 @@ fun TimerOverviewScreen(
) {
// Default Timers, cannot be edited
items(viewModel.getDefaultTimers()) {
TimerEntry(timerInfo = it, canEdit = false)
TimerEntry(timerInfo = it, canDisplay = false)
}
// User timers, can be edited
items(timers.value) {
TimerEntry(timerInfo = it, true) { timerInfo ->
TimerEntry(timerInfo = it, true, R.string.edit) { timerInfo ->
viewModel.update(timerInfo)
}
}
@ -65,7 +66,12 @@ fun TimerOverviewScreen(
}
@Composable
fun TimerEntry(timerInfo: TimerInfo, canEdit: Boolean, update: (TimerInfo) -> Unit = {}) {
fun TimerEntry(
timerInfo: TimerInfo,
canDisplay: Boolean,
@StringRes buttonName: Int = -1,
buttonFunction: (TimerInfo) -> Unit = {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
@ -83,9 +89,9 @@ fun TimerEntry(timerInfo: TimerInfo, canEdit: Boolean, update: (TimerInfo) -> Un
fontSize = 15.sp
)
}
if (canEdit) {
BasicButton(R.string.edit, Modifier.card()) {
// TODO
if (canDisplay) {
BasicButton(buttonName, Modifier.card()) {
buttonFunction(timerInfo)
}
}

View file

@ -0,0 +1,44 @@
package be.ugent.sel.studeez.screens.timer_selection
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.screens.timer_overview.TimerEntry
@Composable
fun TimerSelectionScreen(
open: (String) -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: TimerSelectionViewModel = hiltViewModel()
) {
val timers = viewModel.getAllTimers().collectAsState(initial = emptyList())
PrimaryScreenTemplate(
title = resources().getString(R.string.timers),
open = open,
openAndPopUp = openAndPopUp,
) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp)) {
// All timers
items(timers.value) {
TimerEntry(
timerInfo = it,
canDisplay = true,
buttonName = R.string.start
) { timerInfo ->
viewModel.startSession(open, timerInfo)
}
}
}
}
}

View file

@ -0,0 +1,28 @@
package be.ugent.sel.studeez.screens.timer_selection
import be.ugent.sel.studeez.data.SelectedTimerState
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
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@HiltViewModel
class TimerSelectionViewModel @Inject constructor(
private val timerDAO: TimerDAO,
private val selectedTimerState: SelectedTimerState,
logService: LogService
) : StudeezViewModel(logService) {
fun getAllTimers() : Flow<List<TimerInfo>> {
return timerDAO.getAllTimers()
}
fun startSession(open: (String) -> Unit, timerInfo: TimerInfo) {
selectedTimerState.selectedTimer = timerInfo.getFunctionalTimer()
open(StudeezDestinations.SESSION_SCREEN)
}
}

View file

@ -1,4 +0,0 @@
package be.ugent.sel.studeez.screens.timers
class TimerScreen {
}

View file

@ -13,6 +13,7 @@
<string name="cancel">Cancel</string>
<string name="go_back">Go back</string>
<string name="next">Next</string>
<string name="start">Start</string>
<!-- Messages -->
<string name="success">Success!</string>
@ -63,6 +64,14 @@
<string name="timers">Timers</string>
<string name="edit">Edit</string>
<string name="add_timer">Add timer</string>
<string name="state_focus">Focus!</string>
<plurals name="state_focus_remaining">
<item quantity="zero">Focus one more time!</item>
<item quantity="one">Focus! (%d break remaining)</item>
<item quantity="other">Focus! (%d breaks remaining)</item>
</plurals>
<string name="state_done">Done!</string>
<string name="state_take_a_break">Take a break!</string>
<!-- Settings -->
<string name="settings">Settings</string>

View file

@ -1,17 +0,0 @@
package be.ugent.sel.studeez
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View file

@ -0,0 +1,44 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
class FunctionalCustomTimerUnitTest : FunctionalTimerUnitTest() {
override fun setTimer() {
timer = FunctionalCustomTimer(time)
}
@Test
override fun testOneTick() {
timer.tick()
Assert.assertEquals(
time - 1,
timer.time.time,
)
}
@Test
override fun multipleTicks() {
val n = 10
for (i in 1..n) {
timer.tick()
}
Assert.assertEquals(
time - n,
timer.time.time,
)
}
@Test
override fun testEnded() {
timer = FunctionalCustomTimer(0)
timer.tick()
Assert.assertTrue(timer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.DONE,
timer.view
)
}
}

View file

@ -0,0 +1,46 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
class FunctionalEndlessTimerUnitTest : FunctionalTimerUnitTest() {
override fun setTimer() {
timer = FunctionalEndlessTimer()
}
@Test
override fun testOneTick() {
timer.tick()
Assert.assertEquals(
1,
timer.time.time
)
}
@Test
override fun multipleTicks() {
val n = 10
for (i in 1..n) {
timer.tick()
}
Assert.assertEquals(
n,
timer.time.time
)
}
@Test
override fun testEnded() {
val n = 1000
for (i in 1..n) {
timer.tick()
Assert.assertFalse(timer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS,
timer.view
)
}
}
}

View file

@ -0,0 +1,98 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
private val breakTime = 10
private val breaks = 2
override val hours = 0
override val minutes = 0
override val seconds = 10
private lateinit var pomodoroTimer: FunctionalPomodoroTimer
override fun setTimer() {
pomodoroTimer = FunctionalPomodoroTimer(time, breakTime, breaks)
}
@Test
override fun testOneTick() {
pomodoroTimer.tick()
Assert.assertEquals(
time - 1,
pomodoroTimer.time.time,
)
Assert.assertFalse(pomodoroTimer.isInBreak)
Assert.assertEquals(
breaks,
pomodoroTimer.breaksRemaining,
)
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS,
pomodoroTimer.view,
)
}
@Test
override fun multipleTicks() {
val n = 10
for (i in 1..n) {
pomodoroTimer.tick()
}
Assert.assertEquals(
time - n,
pomodoroTimer.time.time
)
}
@Test
override fun testEnded() {
pomodoroTimer = FunctionalPomodoroTimer(0, 0, 0)
pomodoroTimer.tick()
Assert.assertTrue(pomodoroTimer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.DONE,
pomodoroTimer.view,
)
}
@Test
fun switchToBreak() {
for (i in 0..10) {
pomodoroTimer.tick()
}
Assert.assertFalse(pomodoroTimer.hasEnded())
Assert.assertTrue(pomodoroTimer.isInBreak)
Assert.assertEquals(
FunctionalTimer.StudyState.BREAK,
pomodoroTimer.view
)
}
@Test
fun switchToStudying() {
for (i in 0..time) {
pomodoroTimer.tick()
}
Assert.assertTrue(pomodoroTimer.isInBreak)
Assert.assertEquals(
FunctionalTimer.StudyState.BREAK,
pomodoroTimer.view
)
for (i in 0..breakTime) {
pomodoroTimer.tick()
}
Assert.assertFalse(pomodoroTimer.isInBreak)
val breaksRemaining = breaks - 1
Assert.assertEquals(
breaksRemaining,
pomodoroTimer.breaksRemaining
)
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS_REMAINING,
pomodoroTimer.view
)
}
}

View file

@ -0,0 +1,33 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Before
import org.junit.Test
abstract class FunctionalTimerUnitTest {
protected lateinit var timer: FunctionalTimer
protected open val hours = 4
protected open val minutes = 20
protected open val seconds = 39
protected var time: Int = 0
@Before
fun setup() {
time = seconds + minutes * 60 + hours * 60 * 60
setTimer()
}
/**
* The timer-property should be set to the right implementation in this method.
*/
abstract fun setTimer()
@Test
abstract fun testOneTick()
@Test
abstract fun multipleTicks()
@Test
abstract fun testEnded()
}

View file

@ -0,0 +1,81 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.data.local.models.timer_functional.Time
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class TimeUnitTest {
private val hours = 4
private val minutes = 20
private val seconds = 39
private val time: Time = Time(seconds + minutes * 60 + hours * 60 * 60)
@Before
fun setup() {
}
@Test
fun formatTime() {
Assert.assertEquals(
HoursMinutesSeconds(
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
seconds.toString().padStart(2, '0'),
),
time.getAsHMS(),
)
}
@Test
fun getTime() {
Assert.assertEquals(
seconds + minutes * 60 + hours * 60 * 60,
time.time,
)
}
@Test
fun minOne() {
time.minOne()
Assert.assertEquals(
(seconds + minutes * 60 + hours * 60 * 60) - 1,
time.time,
)
}
@Test
fun plusOne() {
time.plusOne()
Assert.assertEquals(
(seconds + minutes * 60 + hours * 60 * 60) + 1,
time.time,
)
}
@Test
fun minMultiple() {
val n = 10
for (i in 1 .. n) {
time.minOne()
}
Assert.assertEquals(
(seconds + minutes * 60 + hours * 60 * 60) - n,
time.time,
)
}
@Test
fun plusMultiple() {
val n = 10
for (i in 1 .. n) {
time.plusOne()
}
Assert.assertEquals(
(seconds + minutes * 60 + hours * 60 * 60) + n,
time.time,
)
}
}