resolve conflicts

This commit is contained in:
lbarraga 2023-05-15 23:07:40 +02:00
commit 9bc210c3d0
83 changed files with 1867 additions and 788 deletions

View file

@ -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

View file

@ -2,6 +2,7 @@ package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -20,6 +21,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -52,6 +54,7 @@ fun BasicButton(
modifier: Modifier = Modifier,
colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null,
enabled: Boolean = true,
onClick: () -> Unit,
) {
Button(
@ -60,6 +63,7 @@ fun BasicButton(
shape = defaultButtonShape(),
colors = colors,
border = border,
enabled = enabled,
) {
Text(
text = stringResource(text),
@ -78,17 +82,22 @@ fun BasicButtonPreview() {
fun StealthButton(
@StringRes text: Int,
modifier: Modifier = Modifier.card(),
enabled: Boolean = true,
onClick: () -> Unit,
) {
//val clickablemodifier = if (disabled) Modifier.clickable(indication = null) else modifier
val borderColor = if (enabled) MaterialTheme.colors.primary
else MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
BasicButton(
text = text,
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
contentColor = borderColor
),
border = BorderStroke(3.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.4f))
border = BorderStroke(2.dp, borderColor)
)
}

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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)
)
)
}

View file

@ -3,10 +3,13 @@ package be.ugent.sel.studeez.common.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
@ -23,4 +26,14 @@ fun Headline(
fontSize = 34.sp
)
}
}
@Composable
fun DateText(date: String) {
Text(
text = date,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
modifier = Modifier.padding(horizontal = 10.dp)
)
}

View file

@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.background
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
@ -50,7 +49,7 @@ fun LabelledInputField(
value: String,
onNewValue: (String) -> Unit,
@StringRes label: Int,
singleLine: Boolean = false
singleLine: Boolean = true
) {
OutlinedTextField(
value = value,
@ -122,7 +121,9 @@ fun LabeledErrorTextField(
initialValue: String,
@StringRes label: Int,
singleLine: Boolean = false,
errorText: Int,
isValid: MutableState<Boolean> = remember { mutableStateOf(true) },
isFirst: MutableState<Boolean> = remember { mutableStateOf(false) },
@StringRes errorText: Int,
keyboardType: KeyboardType,
predicate: (String) -> Boolean,
onNewCorrectValue: (String) -> Unit
@ -131,31 +132,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),

View file

@ -0,0 +1,160 @@
package be.ugent.sel.studeez.common.composable.feed
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import be.ugent.sel.studeez.common.composable.BasicTextButton
import be.ugent.sel.studeez.common.composable.DateText
import be.ugent.sel.studeez.common.composable.Headline
import be.ugent.sel.studeez.common.ext.textButton
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun Feed(
uiState: FeedUiState,
continueTask: (String, String) -> Unit,
onEmptyFeedHelp: () -> Unit
) {
when (uiState) {
FeedUiState.Loading -> LoadingFeed()
is FeedUiState.Succes -> LoadedFeed(
uiState = uiState,
continueTask = continueTask,
onEmptyFeedHelp = onEmptyFeedHelp
)
}
}
@Composable
fun LoadedFeed(
uiState: FeedUiState.Succes,
continueTask: (String, String) -> Unit,
onEmptyFeedHelp: () -> Unit,
) {
if (uiState.feedEntries.isEmpty()) EmptyFeed(onEmptyFeedHelp)
else FeedWithElements(uiState = uiState, continueTask = continueTask)
}
@Composable
fun LoadingFeed() {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(color = MaterialTheme.colors.onBackground)
}
}
@Composable
fun FeedWithElements(
uiState: FeedUiState.Succes,
continueTask: (String, String) -> Unit,
) {
val feedEntries = uiState.feedEntries
LazyColumn {
items(feedEntries.toList()) { (date, feedEntries) ->
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
val totalDayStudyTime: Int = feedEntries.sumOf { it.totalStudyTime }
DateText(date = date)
Text(
text = "${HoursMinutesSeconds(totalDayStudyTime)}",
fontSize = 15.sp,
fontWeight = FontWeight.Bold
)
}
feedEntries.forEach { feedEntry ->
FeedEntry(feedEntry = feedEntry) {
continueTask(feedEntry.subjectId, feedEntry.taskId)
}
}
Spacer(modifier = Modifier.height(20.dp))
}
}
}
@Composable
fun EmptyFeed(onEmptyFeedHelp: () -> Unit) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Headline(text = stringResource(id = AppText.your_feed))
BasicTextButton(
AppText.empty_feed_help_text,
Modifier.textButton(),
action = onEmptyFeedHelp,
)
}
}
}
@Preview
@Composable
fun FeedLoadingPreview() {
Feed(
uiState = FeedUiState.Loading,
continueTask = { _, _ -> run {} }, {}
)
}
@Preview
@Composable
fun FeedPreview() {
Feed(
uiState = FeedUiState.Succes(
mapOf(
"08 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 600,
),
FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 20,
),
),
"09 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFFD1200,
subJectName = "Test Subject",
taskName = "Test Task",
),
FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
),
)
)
),
continueTask = { _, _ -> run {} }, {}
)
}

View file

@ -0,0 +1,116 @@
package be.ugent.sel.studeez.common.composable.feed
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.composable.StealthButton
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun FeedEntry(
feedEntry: FeedEntry,
continueWithTask: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 10.dp)
.weight(11f)
) {
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color(feedEntry.argb_color)),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
Text(
text = feedEntry.subJectName,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
Text(
text = feedEntry.taskName,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
Text(text = HoursMinutesSeconds(feedEntry.totalStudyTime).toString())
}
}
val buttonText: Int =
if (feedEntry.isArchived) AppText.deleted else AppText.continue_task
StealthButton(
text = buttonText,
enabled = !feedEntry.isArchived,
modifier = Modifier
.padding(start = 10.dp, end = 5.dp)
.weight(6f)
) {
if (!feedEntry.isArchived) {
continueWithTask()
}
}
}
}
}
@Preview
@Composable
fun FeedEntryPreview() {
FeedEntry(
continueWithTask = {},
feedEntry = FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 20,
)
)
}
@Preview
@Composable
fun FeedEntryOverflowPreview() {
FeedEntry(
continueWithTask = {},
feedEntry = FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkk",
totalStudyTime = 20,
)
)
}

View file

@ -0,0 +1,8 @@
package be.ugent.sel.studeez.common.composable.feed
import be.ugent.sel.studeez.data.local.models.FeedEntry
sealed interface FeedUiState {
object Loading : FeedUiState
data class Succes(val feedEntries: Map<String, List<FeedEntry>>) : FeedUiState
}

View file

@ -0,0 +1,45 @@
package be.ugent.sel.studeez.common.composable.feed
import androidx.lifecycle.viewModelScope
import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.TaskDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FeedViewModel @Inject constructor(
feedDAO: FeedDAO,
private val taskDAO: TaskDAO,
private val selectedTask: SelectedTask,
logService: LogService
) : StudeezViewModel(logService) {
val uiState: StateFlow<FeedUiState> = feedDAO.getFeedEntries()
.map { FeedUiState.Succes(it) }
.stateIn(
scope = viewModelScope,
initialValue = FeedUiState.Loading,
started = SharingStarted.Eagerly,
)
fun continueTask(open: (String) -> Unit, subjectId: String, taskId: String) {
viewModelScope.launch {
val task = taskDAO.getTask(subjectId, taskId)
selectedTask.set(task)
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
}
}
fun onEmptyFeedHelp(open: (String) -> Unit) {
open(StudeezDestinations.ADD_SUBJECT_FORM)
}
}

View file

@ -1,13 +1,7 @@
package be.ugent.sel.studeez.common.composable.tasks
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
@ -15,6 +9,8 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -24,16 +20,24 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.R.string as AppText
import be.ugent.sel.studeez.common.composable.StealthButton
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun SubjectEntry(
subject: Subject,
onViewSubject: () -> Unit,
getTaskCount: () -> Flow<Int>,
getCompletedTaskCount: () -> Flow<Int>,
getStudyTime: () -> Flow<Int>,
) {
val studytime by getStudyTime().collectAsState(initial = 0)
val taskCount by getTaskCount().collectAsState(initial = 0)
val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0)
Card(
modifier = Modifier
.fillMaxWidth()
@ -70,7 +74,7 @@ fun SubjectEntry(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = HoursMinutesSeconds(subject.time).toString(),
text = HoursMinutesSeconds(studytime).toString(),
)
Row(
verticalAlignment = Alignment.CenterVertically,
@ -80,7 +84,7 @@ fun SubjectEntry(
imageVector = Icons.Default.List,
contentDescription = stringResource(id = AppText.tasks)
)
Text(text = "0/0") // TODO
Text(text = "${completedTaskCount}/${taskCount}")
}
}
}
@ -104,9 +108,12 @@ fun SubjectEntryPreview() {
subject = Subject(
name = "Test Subject",
argb_color = 0xFFFFD200,
time = 60
),
) {}
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
)
}
@Preview
@ -116,7 +123,10 @@ fun OverflowSubjectEntryPreview() {
subject = Subject(
name = "Testttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt",
argb_color = 0xFFFFD200,
time = 60
),
) {}
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
)
}

View file

@ -1,17 +1,7 @@
package be.ugent.sel.studeez.common.composable.tasks
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
@ -31,7 +21,8 @@ import be.ugent.sel.studeez.resources
fun TaskEntry(
task: Task,
onCheckTask: (Boolean) -> Unit,
onDeleteTask: () -> Unit,
onArchiveTask: () -> Unit,
onStartTask: () -> Unit
) {
Card(
modifier = Modifier
@ -80,7 +71,7 @@ fun TaskEntry(
Box(modifier = Modifier.weight(7f)) {
if (task.completed) {
IconButton(
onClick = onDeleteTask,
onClick = onArchiveTask,
modifier = Modifier
.padding(start = 20.dp)
) {
@ -95,6 +86,7 @@ fun TaskEntry(
modifier = Modifier
.padding(end = 5.dp),
) {
onStartTask()
}
}
}
@ -110,7 +102,7 @@ fun TaskEntryPreview() {
name = "Test Task",
completed = false,
),
{}, {},
{}, {}, {}
)
}
@ -122,7 +114,7 @@ fun CompletedTaskEntryPreview() {
name = "Test Task",
completed = true,
),
{}, {},
{}, {}, {},
)
}
@ -134,6 +126,6 @@ fun OverflowTaskEntryPreview() {
name = "Test Taskkkkkkkkkkkkkkkkkkkkkkkkkkk",
completed = false,
),
{}, {},
{}, {}, {}
)
}

View file

@ -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
}

View file

@ -1,8 +1,12 @@
package be.ugent.sel.studeez.common.ext
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.unit.dp
fun Modifier.textButton(): Modifier {

View file

@ -1,11 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EditTimerState @Inject constructor(){
lateinit var timerInfo: TimerInfo
}

View file

@ -0,0 +1,45 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to cummunicate between viewmodels.
*/
abstract class SelectedState<T> {
abstract var value: T
operator fun invoke() = value
fun set(newValue: T) {
this.value = newValue
}
}
@Singleton
class SelectedSessionReport @Inject constructor() : SelectedState<SessionReport>() {
override lateinit var value: SessionReport
}
@Singleton
class SelectedTask @Inject constructor() : SelectedState<Task>() {
override lateinit var value: Task
}
@Singleton
class SelectedTimer @Inject constructor() : SelectedState<FunctionalTimer>() {
override lateinit var value: FunctionalTimer
}
@Singleton
class SelectedSubject @Inject constructor() : SelectedState<Subject>() {
override lateinit var value: Subject
}
@Singleton
class SelectedTimerInfo @Inject constructor() : SelectedState<TimerInfo>() {
override lateinit var value: TimerInfo
}

View file

@ -1,20 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.task.Subject
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the selected subject from the subject overview other screens.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedSubject @Inject constructor() {
private lateinit var subject: Subject
operator fun invoke() = subject
fun set(subject: Subject) {
this.subject = subject
}
fun isSet() = this::subject.isInitialized
}

View file

@ -1,21 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.task.Task
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the selected task from the task overview other screens.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SelectedTask @Inject constructor() {
private lateinit var task: Task
operator fun invoke() = task
fun set(task: Task) {
this.task = task
}
fun isSet() = this::task.isInitialized
}

View file

@ -1,14 +0,0 @@
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,14 +0,0 @@
package be.ugent.sel.studeez.data
import be.ugent.sel.studeez.data.local.models.SessionReport
import javax.inject.Inject
import javax.inject.Singleton
/**
* Used to communicate the SelectedTimer from the selection screen to the session screen.
* Because this is a singleton-class the view-models of both screens observe the same data.
*/
@Singleton
class SessionReportState @Inject constructor(){
var sessionReport: SessionReport? = null
}

View file

@ -0,0 +1,14 @@
package be.ugent.sel.studeez.data.local.models
import com.google.firebase.Timestamp
data class FeedEntry(
val argb_color: Long = 0,
val subJectName: String = "",
val taskName: String = "",
val taskId: String = "", // Name of task is not unique
val subjectId: String = "",
val totalStudyTime: Int = 0,
val endTime: Timestamp = Timestamp(0, 0),
val isArchived: Boolean = false
)

View file

@ -6,5 +6,7 @@ import com.google.firebase.firestore.DocumentId
data class SessionReport(
@DocumentId val id: String = "",
val studyTime: Int = 0,
val endTime: Timestamp = Timestamp(0, 0)
val endTime: Timestamp = Timestamp(0, 0),
val taskId: String = "",
val subjectId: String = ""
)

View file

@ -1,10 +1,18 @@
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 time: Int = 0,
val argb_color: Long = 0,
)
var archived: Boolean = false,
)
object SubjectDocument {
const val id = "id"
const val name = "name"
const val archived = "archived"
const val argb_color = "argb_color"
}

View file

@ -5,9 +5,10 @@ import com.google.firebase.firestore.DocumentId
data class Task(
@DocumentId val id: String = "",
val name: String = "",
val completed: Boolean = false,
var completed: Boolean = false,
val time: Int = 0,
val subjectId: String = "",
var archived: Boolean = false,
)
object TaskDocument {
@ -16,4 +17,5 @@ object TaskDocument {
const val completed = "completed"
const val time = "time"
const val subjectId = "subjectId"
const val archived = "archived"
}

View file

@ -2,17 +2,17 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
class FunctionalPomodoroTimer(
private var studyTime: Int,
private var breakTime: Int, repeats: Int
private var breakTime: Int,
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--

View file

@ -2,6 +2,7 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
import be.ugent.sel.studeez.data.local.models.SessionReport
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentReference
abstract class FunctionalTimer(initialValue: Int) {
var time: Time = Time(initialValue)
@ -17,10 +18,12 @@ abstract class FunctionalTimer(initialValue: Int) {
abstract fun hasCurrentCountdownEnded(): Boolean
fun getSessionReport(): SessionReport {
fun getSessionReport(subjectId: String, taskId: String): SessionReport {
return SessionReport(
studyTime = totalStudyTime,
endTime = Timestamp.now()
endTime = Timestamp.now(),
taskId = taskId,
subjectId = subjectId
)
}

View file

@ -36,4 +36,7 @@ abstract class DatabaseModule {
@Binds
abstract fun provideTaskDAO(impl: FirebaseTaskDAO): TaskDAO
@Binds
abstract fun provideFeedDAO(impl: FirebaseFeedDAO): FeedDAO
}

View file

@ -0,0 +1,10 @@
package be.ugent.sel.studeez.domain
import be.ugent.sel.studeez.data.local.models.FeedEntry
import kotlinx.coroutines.flow.Flow
interface FeedDAO {
fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>>
}

View file

@ -12,4 +12,12 @@ interface SubjectDAO {
fun deleteSubject(oldSubject: Subject)
fun updateSubject(newSubject: Subject)
suspend fun archiveSubject(subject: Subject)
fun getTaskCount(subject: Subject): Flow<Int>
fun getCompletedTaskCount(subject: Subject): Flow<Int>
fun getStudyTime(subject: Subject): Flow<Int>
suspend fun getSubject(subjectId: String): Subject?
}

View file

@ -14,5 +14,5 @@ interface TaskDAO {
fun deleteTask(oldTask: Task)
fun toggleTaskCompleted(task: Task, completed: Boolean)
suspend fun getTask(subjectId: String, taskId: String): Task
}

View file

@ -0,0 +1,81 @@
package be.ugent.sel.studeez.domain.implementation
import android.icu.text.DateFormat
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.domain.FeedDAO
import be.ugent.sel.studeez.domain.SessionDAO
import be.ugent.sel.studeez.domain.SubjectDAO
import be.ugent.sel.studeez.domain.TaskDAO
import com.google.firebase.Timestamp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class FirebaseFeedDAO @Inject constructor(
private val sessionDAO: SessionDAO,
private val taskDAO: TaskDAO,
private val subjectDAO: SubjectDAO
) : FeedDAO {
/**
* Return a map as with key the day and value a list of feedentries for that day.
*/
override fun getFeedEntries(): Flow<Map<String, List<FeedEntry>>> {
return sessionDAO.getSessions().map { sessionReports ->
sessionReports
.map { sessionReport -> sessionToFeedEntry(sessionReport) }
.sortedByDescending { it.endTime }
.groupBy { getFormattedTime(it) }
.mapValues { (_, entries) ->
entries
.groupBy { it.taskId }
.map { fuseFeedEntries(it.component2()) }
}
}
}
private fun getFormattedTime(entry: FeedEntry): String {
return DateFormat.getDateInstance().format(entry.endTime.toDate())
}
/**
* Givin a list of entries referencing the same task, in the same day, fuse them into one
* feed-entry by adding the studytime and keeping the most recent end-timestamp
*/
private fun fuseFeedEntries(entries: List<FeedEntry>): FeedEntry =
entries.drop(1).fold(entries[0]) { accEntry, newEntry ->
accEntry.copy(
totalStudyTime = accEntry.totalStudyTime + newEntry.totalStudyTime,
endTime = getMostRecent(accEntry.endTime, newEntry.endTime)
)
}
private fun getMostRecent(t1: Timestamp, t2: Timestamp): Timestamp {
return if (t1 < t2) t2 else t1
}
/**
* Convert a sessionReport to a feedEntry. Fetch Task and Subject to get names
*/
private suspend fun sessionToFeedEntry(sessionReport: SessionReport): FeedEntry {
val subjectId: String = sessionReport.subjectId
val taskId: String = sessionReport.taskId
val task: Task = taskDAO.getTask(subjectId, taskId)
val subject: Subject = subjectDAO.getSubject(subjectId)!!
return FeedEntry(
argb_color = subject.argb_color,
subJectName = subject.name,
taskName = task.name,
taskId = task.id,
subjectId = subject.id,
totalStudyTime = sessionReport.studyTime,
endTime = sessionReport.endTime,
isArchived = task.archived || subject.archived
)
}
}

View file

@ -1,25 +1,40 @@
package be.ugent.sel.studeez.domain.implementation
import android.util.Log
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.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.snapshots
import com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import kotlin.collections.count
class FirebaseSubjectDAO @Inject constructor(
private val firestore: FirebaseFirestore,
private val auth: AccountDAO,
private val taskDAO: TaskDAO,
) : SubjectDAO {
override fun getSubjects(): Flow<List<Subject>> {
return currentUserSubjectsCollection()
.subjectNotArchived()
.snapshots()
.map { it.toObjects(Subject::class.java) }
}
override suspend fun getSubject(subjectId: String): Subject? {
return currentUserSubjectsCollection().document(subjectId).get().await().toObject()
}
override fun saveSubject(newSubject: Subject) {
currentUserSubjectsCollection().add(newSubject)
}
@ -32,8 +47,48 @@ class FirebaseSubjectDAO @Inject constructor(
currentUserSubjectsCollection().document(newSubject.id).set(newSubject)
}
override suspend fun archiveSubject(subject: Subject) {
currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true)
currentUserSubjectsCollection().document(subject.id)
.collection(FirebaseCollections.TASK_COLLECTION)
.taskNotArchived()
.get().await()
.documents
.forEach {
it.reference.update(TaskDocument.archived, true)
}
}
override fun getTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map(List<Task>::count)
}
override fun getCompletedTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map { tasks -> tasks.count { it.completed && !it.archived } }
}
override fun getStudyTime(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map { tasks -> tasks.sumOf { it.time } }
}
private fun currentUserSubjectsCollection(): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
private fun subjectTasksCollection(subject: Subject): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subject.id)
.collection(FirebaseCollections.TASK_COLLECTION)
fun CollectionReference.subjectNotArchived(): Query =
this.whereEqualTo(SubjectDocument.archived, false)
fun Query.subjectNotArchived(): Query =
this.whereEqualTo(SubjectDocument.archived, false)
}

View file

@ -7,9 +7,12 @@ import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.TaskDAO
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.snapshots
import com.google.firebase.firestore.ktx.toObject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
class FirebaseTaskDAO @Inject constructor(
@ -18,32 +21,47 @@ class FirebaseTaskDAO @Inject constructor(
) : TaskDAO {
override fun getTasks(subject: Subject): Flow<List<Task>> {
return selectedSubjectTasksCollection(subject.id)
.taskNotArchived()
.snapshots()
.map { it.toObjects(Task::class.java) }
}
override suspend fun getTask(subjectId: String, taskId: String): Task {
return selectedSubjectTasksCollection(subjectId).document(taskId).get().await().toObject()!!
}
override fun saveTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.subjectId).add(newTask)
}
override fun updateTask(newTask: Task) {
selectedSubjectTasksCollection(newTask.id).document(newTask.id).set(newTask)
selectedSubjectTasksCollection(newTask.subjectId)
.document(newTask.id)
.set(newTask)
}
override fun deleteTask(oldTask: Task) {
selectedSubjectTasksCollection(oldTask.subjectId).document(oldTask.id).delete()
}
override fun toggleTaskCompleted(task: Task, completed: Boolean) {
selectedSubjectTasksCollection(task.subjectId)
.document(task.id)
.update(TaskDocument.completed, completed)
}
private fun selectedSubjectTasksCollection(subjectId: String): CollectionReference =
firestore.collection(FirebaseCollections.USER_COLLECTION)
.document(auth.currentUserId)
.collection(FirebaseCollections.SUBJECT_COLLECTION)
.document(subjectId)
.collection(FirebaseCollections.TASK_COLLECTION)
}
}
// Extend CollectionReference and Query with some filters
fun CollectionReference.taskNotArchived(): Query =
this.whereEqualTo(TaskDocument.archived, false)
fun Query.taskNotArchived(): Query =
this.whereEqualTo(TaskDocument.archived, false)
fun CollectionReference.taskNotCompleted(): Query =
this.whereEqualTo(TaskDocument.completed, true)
fun Query.taskNotCompleted(): Query =
this.whereEqualTo(TaskDocument.completed, true)

View file

@ -27,12 +27,12 @@ import be.ugent.sel.studeez.screens.sessions.SessionsRoute
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.tasks.SubjectRoute
import be.ugent.sel.studeez.screens.subjects.SubjectRoute
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.forms.SubjectAddRoute
import be.ugent.sel.studeez.screens.tasks.forms.SubjectEditRoute
import be.ugent.sel.studeez.screens.tasks.forms.TaskAddRoute
import be.ugent.sel.studeez.screens.tasks.forms.TaskEditRoute
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
import be.ugent.sel.studeez.screens.timer_form.TimerEditRoute
import be.ugent.sel.studeez.screens.timer_form.timer_type_select.TimerTypeSelectScreen
@ -54,6 +54,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 =
@ -68,9 +69,10 @@ fun StudeezNavGraph(
composable(StudeezDestinations.HOME_SCREEN) {
HomeRoute(
open,
viewModel = hiltViewModel(),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions
navigationBarActions = navigationBarActions,
feedViewModel = hiltViewModel(),
viewModel = hiltViewModel()
)
}
@ -84,7 +86,7 @@ fun StudeezNavGraph(
}
composable(StudeezDestinations.ADD_SUBJECT_FORM) {
SubjectAddRoute(
SubjectCreateRoute(
goBack = goBack,
openAndPopUp = openAndPopUp,
viewModel = hiltViewModel(),
@ -108,7 +110,7 @@ fun StudeezNavGraph(
}
composable(StudeezDestinations.ADD_TASK_FORM) {
TaskAddRoute(
TaskCreateRoute(
goBack = goBack,
openAndPopUp = openAndPopUp,
viewModel = hiltViewModel(),
@ -203,7 +205,7 @@ fun StudeezNavGraph(
composable(StudeezDestinations.SESSION_RECAP) {
SessionRecapRoute(
openAndPopUp = openAndPopUp,
clearAndNavigate = clearAndNavigate,
viewModel = hiltViewModel()
)
}

View file

@ -5,14 +5,17 @@ import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.BasicButton
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.feed.Feed
import be.ugent.sel.studeez.common.composable.feed.FeedUiState
import be.ugent.sel.studeez.common.composable.feed.FeedViewModel
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.ext.basicButton
import be.ugent.sel.studeez.data.local.models.FeedEntry
import be.ugent.sel.studeez.resources
@Composable
@ -21,21 +24,27 @@ fun HomeRoute(
viewModel: HomeViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
feedViewModel: FeedViewModel,
) {
val feedUiState by feedViewModel.uiState.collectAsState()
HomeScreen(
onStartSessionClick = { viewModel.onStartSessionClick(open) },
onViewFriendsClick = { viewModel.onViewFriendsClick(open) },
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
feedUiState = feedUiState,
continueTask = { subjectId, taskId -> feedViewModel.continueTask(open, subjectId, taskId) },
onEmptyFeedHelp = { feedViewModel.onEmptyFeedHelp(open) }
)
}
@Composable
fun HomeScreen(
onStartSessionClick: () -> Unit,
onViewFriendsClick: () -> Unit,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions
navigationBarActions: NavigationBarActions,
feedUiState: FeedUiState,
continueTask: (String, String) -> Unit,
onEmptyFeedHelp: () -> Unit,
) {
PrimaryScreenTemplate(
title = resources().getString(R.string.home),
@ -43,9 +52,7 @@ fun HomeScreen(
navigationBarActions = navigationBarActions,
barAction = { FriendsAction(onViewFriendsClick) }
) {
BasicButton(R.string.start_session, Modifier.basicButton()) {
onStartSessionClick()
}
Feed(feedUiState, continueTask, onEmptyFeedHelp)
}
}
@ -65,9 +72,40 @@ fun FriendsAction(
@Composable
fun HomeScreenPreview() {
HomeScreen(
onStartSessionClick = {},
onViewFriendsClick = {},
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {})
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
feedUiState = FeedUiState.Succes(
mapOf(
"08 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFABD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 600,
),
FeedEntry(
argb_color = 0xFFFFD200,
subJectName = "Test Subject",
taskName = "Test Task",
totalStudyTime = 20,
),
),
"09 May 2023" to listOf(
FeedEntry(
argb_color = 0xFFFD1200,
subJectName = "Test Subject",
taskName = "Test Task",
),
FeedEntry(
argb_color = 0xFFFF5C89,
subJectName = "Test Subject",
taskName = "Test Task",
),
)
)
),
continueTask = { _, _ -> run {} },
onEmptyFeedHelp = {}
)
}

View file

@ -1,6 +1,4 @@
package be.ugent.sel.studeez.screens.home
import be.ugent.sel.studeez.domain.AccountDAO
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
@ -9,15 +7,11 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val accountDAO: AccountDAO,
logService: LogService
) : StudeezViewModel(logService) {
fun onStartSessionClick(open: (String) -> Unit) {
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
}
fun onViewFriendsClick(open: (String) -> Unit) {
open(StudeezDestinations.FRIENDS_OVERVIEW_SCREEN)
}
}
}

View file

@ -1,6 +1,10 @@
package be.ugent.sel.studeez.screens.session
import android.annotation.SuppressLint
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 +14,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() {

View file

@ -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()
}

View file

@ -1,7 +1,8 @@
package be.ugent.sel.studeez.screens.session
import be.ugent.sel.studeez.data.SelectedTimerState
import be.ugent.sel.studeez.data.SessionReportState
import be.ugent.sel.studeez.data.SelectedSessionReport
import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.data.SelectedTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations
@ -11,23 +12,21 @@ import javax.inject.Inject
@HiltViewModel
class SessionViewModel @Inject constructor(
private val selectedTimerState: SelectedTimerState,
private val sessionReportState: SessionReportState,
private val selectedTimer: SelectedTimer,
private val sessionReport: SelectedSessionReport,
private val selectedTask: SelectedTask,
logService: LogService
) : StudeezViewModel(logService) {
private val task : String = "No task selected" // placeholder for tasks implementation
fun getTimer() : FunctionalTimer {
return selectedTimerState.selectedTimer!!
fun getTimer(): FunctionalTimer {
return selectedTimer()
}
fun getTask(): String {
return task
return selectedTask().name
}
fun endSession(openAndPopUp: (String, String) -> Unit) {
sessionReportState.sessionReport = getTimer().getSessionReport()
sessionReport.set(getTimer().getSessionReport(selectedTask().subjectId, selectedTask().id))
openAndPopUp(StudeezDestinations.SESSION_RECAP, StudeezDestinations.SESSION_SCREEN)
}
}

View file

@ -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)
)
}
}

View file

@ -1,143 +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
)
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color.Blue, RoundedCornerShape(32.dp))
) {
Text(
text = sessionActions.getTask(),
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp)
)
}
}
}
}
@Composable
abstract fun motivationString(): String
abstract fun callMediaPlayer()
}
@Preview
@Composable
fun TimerPreview() {
val sessionScreen = object : AbstractSessionScreen() {
@Composable
override fun motivationString(): String = "Test"
override fun callMediaPlayer() {}
}
sessionScreen.Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}, {}))
}

View file

@ -1,45 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import android.media.MediaPlayer
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class BreakSessionScreen(
private val funPomoDoroTimer: FunctionalPomodoroTimer,
private var mediaplayer: MediaPlayer?
): AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
if (funPomoDoroTimer.isInBreak) {
return resources().getString(AppText.state_take_a_break)
}
if (funPomoDoroTimer.hasEnded()) {
return resources().getString(AppText.state_done)
}
return resources().getQuantityString(
R.plurals.state_focus_remaining,
funPomoDoroTimer.breaksRemaining,
funPomoDoroTimer.breaksRemaining
)
}
override fun callMediaPlayer() {
if (funPomoDoroTimer.hasEnded()) {
mediaplayer?.let { it: MediaPlayer ->
it.setOnCompletionListener {
it.release()
mediaplayer = null
}
it.start()
}
} else if (funPomoDoroTimer.hasCurrentCountdownEnded()) {
mediaplayer?.start()
}
}
}

View file

@ -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)
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}

View file

@ -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() {}
}

View file

@ -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)
}

View file

@ -1,18 +0,0 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import android.media.MediaPlayer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
class GetSessionScreen(private val mediaplayer: MediaPlayer?) : FunctionalTimerVisitor<AbstractSessionScreen> {
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen =
CustomSessionScreen(functionalCustomTimer, mediaplayer)
override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen =
EndlessSessionScreen()
override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen =
BreakSessionScreen(functionalPomodoroTimer, mediaplayer)
}

View file

@ -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
)
}
}
}

View file

@ -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)
)
}

View file

@ -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)
)
}

View file

@ -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,
) },
{},
{},
)
)
}

View file

@ -1,9 +1,11 @@
package be.ugent.sel.studeez.screens.session_recap
import be.ugent.sel.studeez.data.SessionReportState
import be.ugent.sel.studeez.data.SelectedSessionReport
import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.data.local.models.SessionReport
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.SessionDAO
import be.ugent.sel.studeez.domain.TaskDAO
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@ -11,23 +13,26 @@ import javax.inject.Inject
@HiltViewModel
class SessionRecapViewModel @Inject constructor(
sessionReportState: SessionReportState,
private val selectedSessionReport: SelectedSessionReport,
private val sessionDAO: SessionDAO,
private val taskDAO: TaskDAO,
private val selectedTask: SelectedTask,
logService: LogService
) : StudeezViewModel(logService) {
private val report: SessionReport = sessionReportState.sessionReport!!
fun getSessionReport(): SessionReport {
return report
return selectedSessionReport()
}
fun saveSession(open: (String, String) -> Unit) {
fun saveSession(open: (String) -> Unit) {
sessionDAO.saveSession(getSessionReport())
open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP)
val newTask =
selectedTask().copy(time = selectedTask().time + selectedSessionReport().studyTime)
taskDAO.updateTask(newTask)
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)
}
}

View file

@ -0,0 +1,130 @@
package be.ugent.sel.studeez.screens.subjects
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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 be.ugent.sel.studeez.common.composable.NewTaskSubjectButton
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.composable.tasks.SubjectEntry
import be.ugent.sel.studeez.data.local.models.task.Subject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun SubjectRoute(
open: (String) -> Unit,
viewModel: SubjectViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
) {
val uiState by viewModel.uiState.collectAsState()
SubjectScreen(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
onAddSubject = { viewModel.onAddSubject(open) },
onViewSubject = { viewModel.onViewSubject(it, open) },
getTaskCount = viewModel::getTaskCount,
getCompletedTaskCount = viewModel::getCompletedTaskCount,
getStudyTime = viewModel::getStudyTime,
uiState,
)
}
@Composable
fun SubjectScreen(
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
onAddSubject: () -> Unit,
onViewSubject: (Subject) -> Unit,
getTaskCount: (Subject) -> Flow<Int>,
getCompletedTaskCount: (Subject) -> Flow<Int>,
getStudyTime: (Subject) -> Flow<Int>,
uiState: SubjectUiState,
) {
PrimaryScreenTemplate(
title = stringResource(AppText.my_subjects),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
barAction = {},
) {
when (uiState) {
SubjectUiState.Loading -> Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(color = MaterialTheme.colors.onBackground)
}
is SubjectUiState.Succes -> {
Column(
modifier = Modifier.padding(top = 5.dp)
) {
NewTaskSubjectButton(onClick = onAddSubject, AppText.new_subject)
LazyColumn {
items(uiState.subjects) {
SubjectEntry(
subject = it,
onViewSubject = { onViewSubject(it) },
getTaskCount = { getTaskCount(it) },
getCompletedTaskCount = { getCompletedTaskCount(it) },
getStudyTime = { getStudyTime(it) },
)
}
}
}
}
}
}
}
@Preview
@Composable
fun SubjectScreenPreview() {
SubjectScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
onAddSubject = {},
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
uiState = SubjectUiState.Succes(
listOf(
Subject(
name = "Test Subject",
argb_color = 0xFFFFD200,
)
)
)
)
}
@Preview
@Composable
fun SubjectScreenLoadingPreview() {
SubjectScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
onAddSubject = {},
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
uiState = SubjectUiState.Loading,
)
}

View file

@ -0,0 +1,8 @@
package be.ugent.sel.studeez.screens.subjects
import be.ugent.sel.studeez.data.local.models.task.Subject
sealed interface SubjectUiState {
object Loading : SubjectUiState
data class Succes(val subjects: List<Subject>) : SubjectUiState
}

View file

@ -1,5 +1,6 @@
package be.ugent.sel.studeez.screens.tasks
package be.ugent.sel.studeez.screens.subjects
import androidx.lifecycle.viewModelScope
import be.ugent.sel.studeez.data.SelectedSubject
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.domain.LogService
@ -7,7 +8,7 @@ import be.ugent.sel.studeez.domain.SubjectDAO
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 kotlinx.coroutines.flow.*
import javax.inject.Inject
@HiltViewModel
@ -16,12 +17,29 @@ class SubjectViewModel @Inject constructor(
private val selectedSubject: SelectedSubject,
logService: LogService,
) : StudeezViewModel(logService) {
fun addSubject(open: (String) -> Unit) {
val uiState: StateFlow<SubjectUiState> = subjectDAO.getSubjects()
.map { SubjectUiState.Succes(it) }
.stateIn(
scope = viewModelScope,
initialValue = SubjectUiState.Loading,
started = SharingStarted.Eagerly,
)
fun onAddSubject(open: (String) -> Unit) {
open(StudeezDestinations.ADD_SUBJECT_FORM)
}
fun getSubjects(): Flow<List<Subject>> {
return subjectDAO.getSubjects()
fun getTaskCount(subject: Subject): Flow<Int> {
return subjectDAO.getTaskCount(subject)
}
fun getCompletedTaskCount(subject: Subject): Flow<Int> {
return subjectDAO.getCompletedTaskCount(subject)
}
fun getStudyTime(subject: Subject): Flow<Int> {
return subjectDAO.getStudyTime(subject)
}
fun onViewSubject(subject: Subject, open: (String) -> Unit) {

View file

@ -1,28 +1,36 @@
package be.ugent.sel.studeez.screens.tasks.forms
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.FormComposable
import be.ugent.sel.studeez.common.composable.LabelledInputField
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
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
fun SubjectAddRoute(
fun SubjectCreateRoute(
goBack: () -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: SubjectFormViewModel,
viewModel: SubjectCreateFormViewModel,
) {
val uiState by viewModel.uiState
SubjectForm(
@ -31,7 +39,7 @@ fun SubjectAddRoute(
uiState = uiState,
onConfirm = { viewModel.onCreate(openAndPopUp) },
onNameChange = viewModel::onNameChange,
onColorChange = {},
onColorChange = viewModel::onColorChange,
)
}
@ -39,19 +47,22 @@ fun SubjectAddRoute(
fun SubjectEditRoute(
goBack: () -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: SubjectFormViewModel,
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 +74,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 +99,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() {

View file

@ -0,0 +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 = Color.generateRandomArgb(),
)

View file

@ -1,34 +1,30 @@
package be.ugent.sel.studeez.screens.tasks.forms
package be.ugent.sel.studeez.screens.subjects.form
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color
import be.ugent.sel.studeez.common.ext.generateRandomArgb
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
import javax.inject.Inject
@HiltViewModel
class SubjectFormViewModel @Inject constructor(
private val subjectDAO: SubjectDAO,
private val selectedSubject: SelectedSubject,
abstract class SubjectFormViewModel(
protected val subjectDAO: SubjectDAO,
protected val selectedSubject: SelectedSubject,
logService: LogService,
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(
if (selectedSubject.isSet()) SubjectFormUiState(
name = selectedSubject().name,
color = selectedSubject().argb_color
)
else SubjectFormUiState()
)
private set
abstract val uiState: MutableState<SubjectFormUiState>
private val name: String
protected val name: String
get() = uiState.value.name
private val color: Long
protected val color: Long
get() = uiState.value.color
fun onNameChange(newValue: String) {
@ -38,11 +34,15 @@ class SubjectFormViewModel @Inject constructor(
fun onColorChange(newValue: Long) {
uiState.value = uiState.value.copy(color = newValue)
}
}
fun onDelete(openAndPopUp: (String, String) -> Unit) {
subjectDAO.deleteSubject(selectedSubject())
openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM)
}
@HiltViewModel
class SubjectCreateFormViewModel @Inject constructor(
subjectDAO: SubjectDAO,
selectedSubject: SelectedSubject,
logService: LogService,
) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) {
override val uiState = mutableStateOf(SubjectFormUiState())
fun onCreate(openAndPopUp: (String, String) -> Unit) {
val newSubject = Subject(
@ -57,13 +57,35 @@ class SubjectFormViewModel @Inject constructor(
// open(StudeezDestinations.TASKS_SCREEN)
openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.ADD_SUBJECT_FORM)
}
}
@HiltViewModel
class SubjectEditFormViewModel @Inject constructor(
subjectDAO: SubjectDAO,
private val taskDAO: TaskDAO,
selectedSubject: SelectedSubject,
logService: LogService,
) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) {
override val uiState = mutableStateOf(
SubjectFormUiState(
name = selectedSubject().name,
color = selectedSubject().argb_color
)
)
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)
}
}

View file

@ -1,80 +0,0 @@
package be.ugent.sel.studeez.screens.tasks
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.composable.NewTaskSubjectButton
import be.ugent.sel.studeez.common.composable.PrimaryScreenTemplate
import be.ugent.sel.studeez.common.composable.drawer.DrawerActions
import be.ugent.sel.studeez.common.composable.navbar.NavigationBarActions
import be.ugent.sel.studeez.common.composable.tasks.SubjectEntry
import be.ugent.sel.studeez.data.local.models.task.Subject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun SubjectRoute(
open: (String) -> Unit,
viewModel: SubjectViewModel,
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
) {
SubjectScreen(
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
addSubject = { viewModel.addSubject(open) },
getSubjects = viewModel::getSubjects,
onViewSubject = { viewModel.onViewSubject(it, open) },
)
}
@Composable
fun SubjectScreen(
drawerActions: DrawerActions,
navigationBarActions: NavigationBarActions,
addSubject: () -> Unit,
getSubjects: () -> Flow<List<Subject>>,
onViewSubject: (Subject) -> Unit,
) {
PrimaryScreenTemplate(
title = stringResource(AppText.my_subjects),
drawerActions = drawerActions,
navigationBarActions = navigationBarActions,
barAction = {},
) {
val subjects = getSubjects().collectAsState(initial = emptyList())
Column(
modifier = Modifier.padding(top = 5.dp)
) {
LazyColumn {
items(subjects.value) {
SubjectEntry(
subject = it,
onViewSubject = { onViewSubject(it) },
)
}
}
NewTaskSubjectButton(onClick = addSubject, AppText.new_subject)
}
}
}
@Preview
@Composable
fun SubjectScreenPreview() {
SubjectScreen(
drawerActions = DrawerActions({}, {}, {}, {}, {}),
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
addSubject = {},
getSubjects = { flowOf() },
onViewSubject = {},
)
}

View file

@ -27,9 +27,10 @@ data class TaskActions(
val addTask: () -> Unit,
val getSubject: () -> Subject,
val getTasks: () -> Flow<List<Task>>,
val deleteTask: (Task) -> Unit,
val onCheckTask: (Task, Boolean) -> Unit,
val editSubject: () -> Unit,
val startTask: (Task) -> Unit,
val archiveTask: (Task) -> Unit,
)
fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskActions {
@ -37,9 +38,10 @@ fun getTaskActions(viewModel: TaskViewModel, open: (String) -> Unit): TaskAction
addTask = { viewModel.addTask(open) },
getTasks = viewModel::getTasks,
getSubject = viewModel::getSelectedSubject,
deleteTask = viewModel::deleteTask,
onCheckTask = { task, isChecked -> viewModel.toggleTaskCompleted(task, isChecked) },
editSubject = { viewModel.editSubject(open) }
editSubject = { viewModel.editSubject(open) },
startTask = { task -> viewModel.startTask(task, open) },
archiveTask = viewModel::archiveTask
)
}
@ -69,16 +71,25 @@ fun TaskScreen(
Column(
modifier = Modifier.padding(top = 5.dp)
) {
NewTaskSubjectButton(onClick = taskActions.addTask, AppText.new_task)
LazyColumn {
items(tasks.value) {
items(tasks.value.filter { !it.completed }) {
TaskEntry(
task = it,
onCheckTask = { isChecked -> taskActions.onCheckTask(it, isChecked) },
onDeleteTask = { taskActions.deleteTask(it) },
onArchiveTask = { taskActions.archiveTask(it) },
onStartTask = { taskActions.startTask(it) }
)
}
items(tasks.value.filter { it.completed }) {
TaskEntry(
task = it,
onCheckTask = { isChecked -> taskActions.onCheckTask(it, isChecked) },
onArchiveTask = { taskActions.archiveTask(it) },
onStartTask = { taskActions.startTask(it) }
)
}
}
NewTaskSubjectButton(onClick = taskActions.addTask, AppText.new_task)
}
}
}
@ -105,9 +116,10 @@ fun TaskScreenPreview() {
{},
{ Subject(name = "Test Subject") },
{ flowOf() },
{},
{ _, _ -> run {} },
{},
{},
{},
)
)
}

View file

@ -1,10 +1,10 @@
package be.ugent.sel.studeez.screens.tasks
import be.ugent.sel.studeez.data.SelectedSubject
import be.ugent.sel.studeez.data.SelectedTask
import be.ugent.sel.studeez.data.local.models.task.Subject
import be.ugent.sel.studeez.data.local.models.task.Task
import be.ugent.sel.studeez.domain.LogService
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
@ -15,8 +15,8 @@ import javax.inject.Inject
@HiltViewModel
class TaskViewModel @Inject constructor(
private val taskDAO: TaskDAO,
private val subjectDAO: SubjectDAO,
private val selectedSubject: SelectedSubject,
private val selectedTask: SelectedTask,
logService: LogService,
) : StudeezViewModel(logService) {
fun addTask(open: (String) -> Unit) {
@ -27,11 +27,6 @@ class TaskViewModel @Inject constructor(
return taskDAO.getTasks(selectedSubject())
}
fun deleteSubject(open: (String) -> Unit) {
subjectDAO.deleteSubject(selectedSubject())
open(StudeezDestinations.SUBJECT_SCREEN)
}
fun getSelectedSubject(): Subject {
return selectedSubject()
}
@ -40,11 +35,20 @@ class TaskViewModel @Inject constructor(
taskDAO.deleteTask(task)
}
fun archiveTask(task: Task) {
taskDAO.updateTask(task.copy(archived = true))
}
fun toggleTaskCompleted(task: Task, completed: Boolean) {
taskDAO.toggleTaskCompleted(task, completed)
taskDAO.updateTask(task.copy(completed = completed))
}
fun editSubject(open: (String) -> Unit) {
open(StudeezDestinations.EDIT_SUBJECT_FORM)
}
fun startTask(task: Task, open: (String) -> Unit) {
selectedTask.set(task)
open(StudeezDestinations.TIMER_SELECTION_SCREEN)
}
}

View file

@ -1,4 +1,4 @@
package be.ugent.sel.studeez.screens.tasks.forms
package be.ugent.sel.studeez.screens.tasks.form
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
@ -11,17 +11,17 @@ 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
import be.ugent.sel.studeez.R.string as AppText
@Composable
fun TaskAddRoute(
fun TaskCreateRoute(
goBack: () -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: TaskFormViewModel,
viewModel: TaskCreateFormViewModel,
) {
val uiState by viewModel.uiState
TaskForm(
@ -37,7 +37,7 @@ fun TaskAddRoute(
fun TaskEditRoute(
goBack: () -> Unit,
openAndPopUp: (String, String) -> Unit,
viewModel: TaskFormViewModel,
viewModel: TaskEditFormViewModel,
) {
val uiState by viewModel.uiState
TaskForm(
@ -62,7 +62,7 @@ fun TaskForm(
onNameChange: (String) -> Unit,
extraButton: @Composable () -> Unit = {}
) {
SecondaryScreenTemplate(
FormComposable(
title = resources().getString(title),
popUp = goBack,
) {

View file

@ -1,4 +1,4 @@
package be.ugent.sel.studeez.screens.tasks.forms
package be.ugent.sel.studeez.screens.tasks.form
data class TaskFormUiState(
val name: String = "",

View file

@ -1,5 +1,6 @@
package be.ugent.sel.studeez.screens.tasks.forms
package be.ugent.sel.studeez.screens.tasks.form
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import be.ugent.sel.studeez.data.SelectedSubject
import be.ugent.sel.studeez.data.SelectedTask
@ -11,39 +12,55 @@ import be.ugent.sel.studeez.screens.StudeezViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class TaskFormViewModel @Inject constructor(
private val taskDAO: TaskDAO,
private val selectedSubject: SelectedSubject,
private val selectedTask: SelectedTask,
abstract class TaskFormViewModel(
protected val taskDAO: TaskDAO,
protected val selectedSubject: SelectedSubject,
protected val selectedTask: SelectedTask,
logService: LogService,
) : StudeezViewModel(logService) {
var uiState = mutableStateOf(
if (selectedTask.isSet()) TaskFormUiState(selectedTask().name) else TaskFormUiState()
)
private set
abstract val uiState: MutableState<TaskFormUiState>
private val name: String
protected val name: String
get() = uiState.value.name
fun onNameChange(newValue: String) {
uiState.value = uiState.value.copy(name = newValue)
}
}
fun onDelete(openAndPopUp: (String, String) -> Unit) {
taskDAO.deleteTask(selectedTask())
openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM)
}
@HiltViewModel
class TaskCreateFormViewModel @Inject constructor(
taskDAO: TaskDAO,
selectedSubject: SelectedSubject,
selectedTask: SelectedTask,
logService: LogService,
) : TaskFormViewModel(taskDAO, selectedSubject, selectedTask, logService) {
override val uiState = mutableStateOf(TaskFormUiState())
fun onCreate(openAndPopUp: (String, String) -> Unit) {
val newTask = Task(name = name, subjectId = selectedSubject().id)
taskDAO.saveTask(newTask)
openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.ADD_TASK_FORM)
}
}
@HiltViewModel
class TaskEditFormViewModel @Inject constructor(
taskDAO: TaskDAO,
selectedSubject: SelectedSubject,
selectedTask: SelectedTask,
logService: LogService,
) : TaskFormViewModel(taskDAO, selectedSubject, selectedTask, logService) {
override val uiState = mutableStateOf(TaskFormUiState())
fun onDelete(openAndPopUp: (String, String) -> Unit) {
taskDAO.deleteTask(selectedTask())
openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM)
}
fun onEdit(openAndPopUp: (String, String) -> Unit) {
val newTask = Task(name = name)
val newTask = selectedTask().copy(name = name)
taskDAO.updateTask(newTask)
openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_TASK_FORM)
}
}
}

View file

@ -1,6 +0,0 @@
package be.ugent.sel.studeez.screens.tasks.forms
data class SubjectFormUiState(
val name: String = "",
val color: Long = 0xFFFFD200,
)

View file

@ -1,42 +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 be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
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)
}
}

View file

@ -0,0 +1,69 @@
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.SecondaryScreenTemplate
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)
}
}

View file

@ -1,6 +1,6 @@
package be.ugent.sel.studeez.screens.timer_form
import be.ugent.sel.studeez.data.EditTimerState
import be.ugent.sel.studeez.data.SelectedTimerInfo
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
@ -10,15 +10,12 @@ import javax.inject.Inject
@HiltViewModel
class TimerFormViewModel @Inject constructor(
private val editTimerState: EditTimerState,
private val selectedTimerInfo: SelectedTimerInfo,
private val timerDAO: TimerDAO,
logService: LogService
) : StudeezViewModel(logService) {
private val timerInfo: TimerInfo = editTimerState.timerInfo
fun getTimerInfo(): TimerInfo {
return timerInfo
return selectedTimerInfo()
}
fun editTimer(timerInfo: TimerInfo, goBack: () -> Unit) {
@ -26,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()

View file

@ -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.

View file

@ -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() {

View file

@ -1,13 +1,13 @@
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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.*
@ -37,7 +37,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)
}
}

View file

@ -1,6 +1,6 @@
package be.ugent.sel.studeez.screens.timer_form.timer_type_select
import be.ugent.sel.studeez.data.EditTimerState
import be.ugent.sel.studeez.data.SelectedTimerInfo
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.navigation.StudeezDestinations
@ -10,13 +10,13 @@ import javax.inject.Inject
@HiltViewModel
class TimerTypeSelectViewModel @Inject constructor(
private val editTimerState: EditTimerState,
private val selectedTimerInfo: SelectedTimerInfo,
logService: LogService
) : StudeezViewModel(logService) {
fun onTimerTypeChosen(timerInfo: TimerInfo, open: (String) -> Unit) {
editTimerState.timerInfo = timerInfo
selectedTimerInfo.set(timerInfo)
open(StudeezDestinations.ADD_TIMER_SCREEN)
}
}

View file

@ -82,12 +82,13 @@ fun TimerOverviewScreen(
items(timers.value) { timerInfo ->
TimerEntry(
timerInfo = timerInfo,
) {
StealthButton(
text = R.string.edit,
onClick = { timerOverviewActions.onEditClick(timerInfo) }
)
}
rightButton = {
StealthButton(
text = R.string.edit,
onClick = { timerOverviewActions.onEditClick(timerInfo) }
)
}
)
}

View file

@ -1,6 +1,6 @@
package be.ugent.sel.studeez.screens.timer_overview
import be.ugent.sel.studeez.data.EditTimerState
import be.ugent.sel.studeez.data.SelectedTimerInfo
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.domain.ConfigurationService
import be.ugent.sel.studeez.domain.LogService
@ -15,11 +15,11 @@ import javax.inject.Inject
class TimerOverviewViewModel @Inject constructor(
private val configurationService: ConfigurationService,
private val timerDAO: TimerDAO,
private val editTimerState: EditTimerState,
private val selectedTimerInfo: SelectedTimerInfo,
logService: LogService
) : StudeezViewModel(logService) {
fun getUserTimers() : Flow<List<TimerInfo>> {
fun getUserTimers(): Flow<List<TimerInfo>> {
return timerDAO.getUserTimers()
}
@ -27,8 +27,8 @@ class TimerOverviewViewModel @Inject constructor(
return configurationService.getDefaultTimers()
}
fun update(timerInfo: TimerInfo, open: (String) -> Unit) {
editTimerState.timerInfo = timerInfo
fun update(timerInfo: TimerInfo, open: (String) -> Unit) {
selectedTimerInfo.set(timerInfo)
open(StudeezDestinations.TIMER_EDIT_SCREEN)
}
@ -36,7 +36,7 @@ class TimerOverviewViewModel @Inject constructor(
open(StudeezDestinations.TIMER_TYPE_CHOOSING_SCREEN)
}
fun delete(timerInfo: TimerInfo) =timerDAO.deleteTimer(timerInfo)
fun delete(timerInfo: TimerInfo) = timerDAO.deleteTimer(timerInfo)
fun save(timerInfo: TimerInfo) = timerDAO.saveTimer(timerInfo)

View file

@ -1,10 +1,13 @@
package be.ugent.sel.studeez.screens.timer_selection
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.common.composable.StealthButton
@ -99,7 +102,10 @@ fun CustomTimerEntry(
)
},
rightButton = {
TimePickerButton(initialSeconds = hms.getTotalSeconds()) { chosenTime ->
TimePickerButton(
initialSeconds = hms.getTotalSeconds(),
modifier = Modifier.padding(horizontal = 5.dp)
) { chosenTime ->
timerInfo.studyTime = chosenTime
}
}

View file

@ -1,10 +1,9 @@
package be.ugent.sel.studeez.screens.timer_selection
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import be.ugent.sel.studeez.data.SelectedTimerState
import be.ugent.sel.studeez.data.SelectedTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.HoursMinutesSeconds
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
import be.ugent.sel.studeez.domain.LogService
import be.ugent.sel.studeez.domain.TimerDAO
@ -17,18 +16,20 @@ import javax.inject.Inject
@HiltViewModel
class TimerSelectionViewModel @Inject constructor(
private val timerDAO: TimerDAO,
private val selectedTimerState: SelectedTimerState,
private val selectedTimer: SelectedTimer,
logService: LogService
) : StudeezViewModel(logService) {
var customTimerStudyTime: MutableState<Int> = mutableStateOf(0)
var customTimerStudyTime: MutableState<Int> = mutableStateOf(
HoursMinutesSeconds(1, 0, 0).getTotalSeconds()
)
fun getAllTimers() : Flow<List<TimerInfo>> {
fun getAllTimers(): Flow<List<TimerInfo>> {
return timerDAO.getAllTimers()
}
fun startSession(open: (String) -> Unit, timerInfo: TimerInfo) {
selectedTimerState.selectedTimer = timerInfo.getFunctionalTimer()
selectedTimer.set(timerInfo.getFunctionalTimer())
open(StudeezDestinations.SESSION_SCREEN)
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="75dp" android:tint="#999999"
android:viewportHeight="24" android:viewportWidth="24"
android:width="75dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="75dp" android:tint="#999999"
android:viewportHeight="24" android:viewportWidth="24"
android:width="75dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,14c-2.33,0 -4.31,1.46 -5.11,3.5h10.22c-0.8,-2.04 -2.78,-3.5 -5.11,-3.5z"/>
</vector>

View file

@ -30,6 +30,12 @@
<string name="home">Home</string>
<string name="start_session">Start session</string>
<!-- Feed-->
<string name="continue_task">Continue</string>
<string name="deleted">Deleted</string>
<string name="your_feed">This is your feed</string>
<string name="empty_feed_help_text">Click here to create you first subject and tasks to get started</string>
<!-- Tasks -->
<string name="tasks">Tasks</string>
<string name="task">Task</string>
@ -41,6 +47,7 @@
<string name="delete_subject">Delete Subject</string>
<string name="delete_task">Delete Task</string>
<string name="view_tasks">View</string>
<string name="regenerate_color">Regenerate Color</string>
<!-- Sessions -->
<string name="sessions_temp_description">Looks like you found the sessions screen! In here, your upcoming studying sessions with friends will be listed. You can accept invites or edit your own.</string> <!-- TODO Remove this description line once implemented. -->
@ -65,8 +72,15 @@
<!-- Timers -->
<string name="timers">Timers</string>
<string name="delete_timer">Delete Timer</string>
<string name="edit">Edit</string>
<string name="add_timer">Add timer</string>
<string name="name_error">Name should not be blank</string>
<string name="description_error">Description should not be blank</string>
<string name="fill_out_error">Fill out all the fields correctly!</string>
<string name="pick_time">Select time</string>
<string name="state_focus">Focus!</string>
<plurals name="state_focus_remaining">
@ -154,4 +168,11 @@
<string name="breakTime">Break Time</string>
<string name="repeats">Number of Repeats</string>
<!-- Session Recap -->
<string name="congrats">"Congratulations! You studied: %s"</string>
<string name="how_did_it_go">How did it go?</string>
<string name="good">Good</string>
<string name="bad">Bad</string>
</resources>

View file

@ -1,11 +1,13 @@
package be.ugent.sel.studeez.timer_functional
import android.media.MediaPlayer
import be.ugent.sel.studeez.data.SelectedTimerState
import be.ugent.sel.studeez.data.SessionReportState
import be.ugent.sel.studeez.data.SelectedSessionReport
import be.ugent.sel.studeez.data.SelectedTimer
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.domain.LogService
import be.ugent.sel.studeez.domain.implementation.LogServiceImpl
import be.ugent.sel.studeez.screens.session.InvisibleSessionManager
import be.ugent.sel.studeez.screens.session.SessionViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -18,14 +20,14 @@ import org.mockito.kotlin.mock
@ExperimentalCoroutinesApi
class InvisibleSessionManagerTest {
private var timerState: SelectedTimerState = SelectedTimerState()
private var selectedTimer: SelectedTimer = SelectedTimer()
private lateinit var viewModel: SessionViewModel
private var mediaPlayer: MediaPlayer = mock()
@Test
fun InvisibleEndlessTimerTest() = runTest {
timerState.selectedTimer = FunctionalEndlessTimer()
viewModel = SessionViewModel(timerState, SessionReportState(), mock())
selectedTimer.set(FunctionalEndlessTimer())
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
val test = launch {
@ -46,8 +48,8 @@ class InvisibleSessionManagerTest {
val studyTime = 10
val breakTime = 5
val repeats = 1
timerState.selectedTimer = FunctionalPomodoroTimer(studyTime, breakTime, repeats)
viewModel = SessionViewModel(timerState, SessionReportState(), mock())
selectedTimer.set(FunctionalPomodoroTimer(studyTime, breakTime, repeats))
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
val test = launch {
@ -79,8 +81,8 @@ class InvisibleSessionManagerTest {
@Test
fun InvisibleCustomTimerTest() = runTest {
timerState.selectedTimer = FunctionalCustomTimer(5)
viewModel = SessionViewModel(timerState, SessionReportState(), mock())
selectedTimer.set(FunctionalCustomTimer(5))
viewModel = SessionViewModel(selectedTimer, SelectedSessionReport(), mock(), LogServiceImpl())
InvisibleSessionManager.setParameters(viewModel, mediaPlayer)
val test = launch {