resolve merge conflicts

This commit is contained in:
lbarraga 2023-05-15 21:47:28 +02:00
commit 924e136153
30 changed files with 469 additions and 184 deletions

1
.idea/misc.xml generated
View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

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,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,7 +3,6 @@ package be.ugent.sel.studeez.common.composable
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@ -22,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.common.ext.fieldModifier
import be.ugent.sel.studeez.resources
import kotlin.math.sin
import be.ugent.sel.studeez.R.drawable as AppIcon
import be.ugent.sel.studeez.R.string as AppText
@ -47,7 +45,7 @@ fun LabelledInputField(
value: String,
onNewValue: (String) -> Unit,
@StringRes label: Int,
singleLine: Boolean = false
singleLine: Boolean = true
) {
OutlinedTextField(
value = value,
@ -119,7 +117,9 @@ fun LabeledErrorTextField(
initialValue: String,
@StringRes label: Int,
singleLine: Boolean = false,
errorText: Int,
isValid: MutableState<Boolean> = remember { mutableStateOf(true) },
isFirst: MutableState<Boolean> = remember { mutableStateOf(false) },
@StringRes errorText: Int,
keyboardType: KeyboardType,
predicate: (String) -> Boolean,
onNewCorrectValue: (String) -> Unit
@ -128,31 +128,28 @@ fun LabeledErrorTextField(
mutableStateOf(initialValue)
}
var isValid by remember {
mutableStateOf(predicate(value))
}
Column {
OutlinedTextField(
modifier = modifier.fieldModifier(),
value = value,
onValueChange = { newText ->
isFirst.value = false
value = newText
isValid = predicate(value)
if (isValid) {
isValid.value = predicate(value)
if (isValid.value) {
onNewCorrectValue(newText)
}
},
singleLine = singleLine,
label = { Text(text = stringResource(id = label)) },
isError = !isValid,
isError = !isValid.value && !isFirst.value,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = ImeAction.Done
)
)
if (!isValid) {
if (!isValid.value && !isFirst.value) {
Text(
modifier = Modifier.padding(start = 16.dp),
text = stringResource(id = errorText),

View file

@ -31,9 +31,13 @@ import be.ugent.sel.studeez.R.string as AppText
fun SubjectEntry(
subject: Subject,
onViewSubject: () -> Unit,
getTaskCount: () -> Flow<Int>,
getCompletedTaskCount: () -> Flow<Int>,
getStudyTime: () -> Flow<Int>,
) {
val studytime by getStudyTime().collectAsState(initial = 0)
val taskCount by getTaskCount().collectAsState(initial = 0)
val completedTaskCount by getCompletedTaskCount().collectAsState(initial = 0)
Card(
modifier = Modifier
.fillMaxWidth()
@ -80,7 +84,7 @@ fun SubjectEntry(
imageVector = Icons.Default.List,
contentDescription = stringResource(id = AppText.tasks)
)
Text(text = "${subject.taskCompletedCount}/${subject.taskCount}")
Text(text = "${completedTaskCount}/${taskCount}")
}
}
}
@ -104,11 +108,11 @@ fun SubjectEntryPreview() {
subject = Subject(
name = "Test Subject",
argb_color = 0xFFFFD200,
taskCount = 5,
taskCompletedCount = 2,
),
onViewSubject = {},
getStudyTime = { flowOf() }
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
)
}
@ -121,6 +125,8 @@ fun OverflowSubjectEntryPreview() {
argb_color = 0xFFFFD200,
),
onViewSubject = {},
getStudyTime = { flowOf() }
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
)
}

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

@ -8,10 +8,6 @@ data class Subject(
val name: String = "",
val argb_color: Long = 0,
var archived: Boolean = false,
@get:Exclude @set:Exclude
var taskCount: Int = 0,
@get:Exclude @set:Exclude
var taskCompletedCount: Int = 0,
)
object SubjectDocument {

View file

@ -13,7 +13,6 @@ class FunctionalPomodoroTimer(
if (hasEnded()) {
return
}
if (hasCurrentCountdownEnded()) {
if (isInBreak) {
breaksRemaining--

View file

@ -13,8 +13,10 @@ interface SubjectDAO {
fun updateSubject(newSubject: Subject)
suspend fun getTaskCount(subject: Subject): Int
suspend fun getCompletedTaskCount(subject: Subject): Int
suspend fun archiveSubject(subject: Subject)
fun getTaskCount(subject: Subject): Flow<Int>
fun getCompletedTaskCount(subject: Subject): Flow<Int>
fun getStudyTime(subject: Subject): Flow<Int>
suspend fun getSubject(subjectId: String): Subject?

View file

@ -1,11 +1,13 @@
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.AggregateSource
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import kotlin.collections.count
class FireBaseSubjectDAO @Inject constructor(
private val firestore: FirebaseFirestore,
@ -26,13 +29,6 @@ class FireBaseSubjectDAO @Inject constructor(
.subjectNotArchived()
.snapshots()
.map { it.toObjects(Subject::class.java) }
.map { subjects ->
subjects.map { subject ->
subject.taskCount = getTaskCount(subject)
subject.taskCompletedCount = getCompletedTaskCount(subject)
subject
}
}
}
override suspend fun getSubject(subjectId: String): Subject? {
@ -51,23 +47,26 @@ class FireBaseSubjectDAO @Inject constructor(
currentUserSubjectsCollection().document(newSubject.id).set(newSubject)
}
override suspend fun getTaskCount(subject: Subject): Int {
return subjectTasksCollection(subject)
override suspend fun archiveSubject(subject: Subject) {
currentUserSubjectsCollection().document(subject.id).update(SubjectDocument.archived, true)
currentUserSubjectsCollection().document(subject.id)
.collection(FireBaseCollections.TASK_COLLECTION)
.taskNotArchived()
.count()
.get(AggregateSource.SERVER)
.await()
.count.toInt()
.get().await()
.documents
.forEach {
it.reference.update(TaskDocument.archived, true)
}
}
override suspend fun getCompletedTaskCount(subject: Subject): Int {
return subjectTasksCollection(subject)
.taskNotArchived()
.taskNotCompleted()
.count()
.get(AggregateSource.SERVER)
.await()
.count.toInt()
override fun getTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map(List<Task>::count)
}
override fun getCompletedTaskCount(subject: Subject): Flow<Int> {
return taskDAO.getTasks(subject)
.map { tasks -> tasks.count { it.completed && !it.archived } }
}
override fun getStudyTime(subject: Subject): Flow<Int> {

View file

@ -25,9 +25,9 @@ import be.ugent.sel.studeez.screens.settings.SettingsRoute
import be.ugent.sel.studeez.screens.sign_up.SignUpRoute
import be.ugent.sel.studeez.screens.splash.SplashRoute
import be.ugent.sel.studeez.screens.subjects.SubjectRoute
import be.ugent.sel.studeez.screens.tasks.TaskRoute
import be.ugent.sel.studeez.screens.subjects.form.SubjectCreateRoute
import be.ugent.sel.studeez.screens.subjects.form.SubjectEditRoute
import be.ugent.sel.studeez.screens.tasks.TaskRoute
import be.ugent.sel.studeez.screens.tasks.form.TaskCreateRoute
import be.ugent.sel.studeez.screens.tasks.form.TaskEditRoute
import be.ugent.sel.studeez.screens.timer_form.TimerAddRoute
@ -51,6 +51,7 @@ fun StudeezNavGraph(
val open: (String) -> Unit = { appState.navigate(it) }
val openAndPopUp: (String, String) -> Unit =
{ route, popUp -> appState.navigateAndPopUp(route, popUp) }
val clearAndNavigate: (route: String) -> Unit = { route -> appState.clearAndNavigate(route) }
val drawerActions: DrawerActions = getDrawerActions(drawerViewModel, open, openAndPopUp)
val navigationBarActions: NavigationBarActions =
@ -200,7 +201,7 @@ fun StudeezNavGraph(
composable(StudeezDestinations.SESSION_RECAP) {
SessionRecapRoute(
openAndPopUp = openAndPopUp,
clearAndNavigate = clearAndNavigate,
viewModel = hiltViewModel()
)
}

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

@ -24,15 +24,15 @@ class SessionRecapViewModel @Inject constructor(
return selectedSessionReport()
}
fun saveSession(open: (String, String) -> Unit) {
fun saveSession(open: (String) -> Unit) {
sessionDAO.saveSession(getSessionReport())
val newTask =
selectedTask().copy(time = selectedTask().time + selectedSessionReport().studyTime)
taskDAO.updateTask(newTask)
open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP)
open(StudeezDestinations.HOME_SCREEN)
}
fun discardSession(open: (String, String) -> Unit) {
open(StudeezDestinations.HOME_SCREEN, StudeezDestinations.SESSION_RECAP)
fun discardSession(open: (String) -> Unit) {
open(StudeezDestinations.HOME_SCREEN)
}
}

View file

@ -36,6 +36,8 @@ fun SubjectRoute(
navigationBarActions = navigationBarActions,
onAddSubject = { viewModel.onAddSubject(open) },
onViewSubject = { viewModel.onViewSubject(it, open) },
getTaskCount = viewModel::getTaskCount,
getCompletedTaskCount = viewModel::getCompletedTaskCount,
getStudyTime = viewModel::getStudyTime,
uiState,
)
@ -47,6 +49,8 @@ fun SubjectScreen(
navigationBarActions: NavigationBarActions,
onAddSubject: () -> Unit,
onViewSubject: (Subject) -> Unit,
getTaskCount: (Subject) -> Flow<Int>,
getCompletedTaskCount: (Subject) -> Flow<Int>,
getStudyTime: (Subject) -> Flow<Int>,
uiState: SubjectUiState,
) {
@ -76,6 +80,8 @@ fun SubjectScreen(
SubjectEntry(
subject = it,
onViewSubject = { onViewSubject(it) },
getTaskCount = { getTaskCount(it) },
getCompletedTaskCount = { getCompletedTaskCount(it) },
getStudyTime = { getStudyTime(it) },
)
}
@ -94,13 +100,14 @@ fun SubjectScreenPreview() {
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
onAddSubject = {},
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
uiState = SubjectUiState.Succes(
listOf(
Subject(
name = "Test Subject",
argb_color = 0xFFFFD200,
taskCount = 5, taskCompletedCount = 2,
)
)
)
@ -115,7 +122,9 @@ fun SubjectScreenLoadingPreview() {
navigationBarActions = NavigationBarActions({ false }, {}, {}, {}, {}, {}, {}, {}),
onAddSubject = {},
onViewSubject = {},
getTaskCount = { flowOf() },
getCompletedTaskCount = { flowOf() },
getStudyTime = { flowOf() },
uiState = SubjectUiState.Loading
uiState = SubjectUiState.Loading,
)
}

View file

@ -30,6 +30,14 @@ class SubjectViewModel @Inject constructor(
open(StudeezDestinations.ADD_SUBJECT_FORM)
}
fun getTaskCount(subject: Subject): Flow<Int> {
return subjectDAO.getTaskCount(subject)
}
fun getCompletedTaskCount(subject: Subject): Flow<Int> {
return subjectDAO.getCompletedTaskCount(subject)
}
fun getStudyTime(subject: Subject): Flow<Int> {
return subjectDAO.getStudyTime(subject)
}

View file

@ -2,20 +2,28 @@ 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
@ -31,7 +39,7 @@ fun SubjectCreateRoute(
uiState = uiState,
onConfirm = { viewModel.onCreate(openAndPopUp) },
onNameChange = viewModel::onNameChange,
onColorChange = {},
onColorChange = viewModel::onColorChange,
)
}
@ -42,16 +50,19 @@ fun SubjectEditRoute(
viewModel: SubjectEditFormViewModel,
) {
val uiState by viewModel.uiState
val coroutineScope = rememberCoroutineScope()
SubjectForm(
title = AppText.edit_subject,
goBack = goBack,
uiState = uiState,
onConfirm = { viewModel.onEdit(openAndPopUp) },
onNameChange = viewModel::onNameChange,
onColorChange = {},
onColorChange = viewModel::onColorChange,
) {
DeleteButton(text = AppText.delete_subject) {
viewModel.onDelete(openAndPopUp)
coroutineScope.launch {
viewModel.onDelete(openAndPopUp)
}
}
}
}
@ -63,21 +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

@ -1,6 +1,9 @@
package be.ugent.sel.studeez.screens.subjects.form
import androidx.compose.ui.graphics.Color
import be.ugent.sel.studeez.common.ext.generateRandomArgb
data class SubjectFormUiState(
val name: String = "",
val color: Long = 0xFFFFD200,
val color: Long = Color.generateRandomArgb(),
)

View file

@ -2,10 +2,13 @@ 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
@ -59,6 +62,7 @@ class SubjectCreateFormViewModel @Inject constructor(
@HiltViewModel
class SubjectEditFormViewModel @Inject constructor(
subjectDAO: SubjectDAO,
private val taskDAO: TaskDAO,
selectedSubject: SelectedSubject,
logService: LogService,
) : SubjectFormViewModel(subjectDAO, selectedSubject, logService) {
@ -69,17 +73,19 @@ class SubjectEditFormViewModel @Inject constructor(
)
)
fun onDelete(openAndPopUp: (String, String) -> Unit) {
subjectDAO.updateSubject(selectedSubject().copy(archived = true))
suspend fun onDelete(openAndPopUp: (String, String) -> Unit) {
subjectDAO.archiveSubject(selectedSubject())
openAndPopUp(StudeezDestinations.SUBJECT_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM)
}
fun onEdit(openAndPopUp: (String, String) -> Unit) {
val newSubject = selectedSubject().copy(
name = name,
argb_color = color,
selectedSubject.set(
selectedSubject().copy(
name = name,
argb_color = color,
)
)
subjectDAO.updateSubject(newSubject)
subjectDAO.updateSubject(selectedSubject())
openAndPopUp(StudeezDestinations.TASKS_SCREEN, StudeezDestinations.EDIT_SUBJECT_FORM)
}
}

View file

@ -11,7 +11,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import be.ugent.sel.studeez.common.composable.BasicButton
import be.ugent.sel.studeez.common.composable.DeleteButton
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
import be.ugent.sel.studeez.common.composable.FormComposable
import be.ugent.sel.studeez.common.ext.basicButton
import be.ugent.sel.studeez.common.ext.fieldModifier
import be.ugent.sel.studeez.resources
@ -62,7 +62,7 @@ fun TaskForm(
onNameChange: (String) -> Unit,
extraButton: @Composable () -> Unit = {}
) {
SecondaryScreenTemplate(
FormComposable(
title = resources().getString(title),
popUp = goBack,
) {

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

@ -23,6 +23,11 @@ class TimerFormViewModel @Inject constructor(
goBack()
}
fun deleteTimer(timerInfo: TimerInfo, goBack: () -> Unit) {
timerDAO.deleteTimer(timerInfo)
goBack()
}
fun saveTimer(timerInfo: TimerInfo, goBack: () -> Unit) {
timerDAO.saveTimer(timerInfo)
goBack()

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

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

@ -46,6 +46,7 @@
<string name="delete_subject">Delete Subject</string>
<string name="delete_task">Delete Task</string>
<string name="view_tasks">View</string>
<string name="regenerate_color">Regenerate Color</string>
<!-- Sessions -->
<string name="sessions_temp_description">Looks like you found the sessions screen! In here, your upcoming studying sessions with friends will be listed. You can accept invites or edit your own.</string> <!-- TODO Remove this description line once implemented. -->
@ -69,8 +70,15 @@
<!-- Timers -->
<string name="timers">Timers</string>
<string name="delete_timer">Delete Timer</string>
<string name="edit">Edit</string>
<string name="add_timer">Add timer</string>
<string name="name_error">Name should not be blank</string>
<string name="description_error">Description should not be blank</string>
<string name="fill_out_error">Fill out all the fields correctly!</string>
<string name="pick_time">Select time</string>
<string name="state_focus">Focus!</string>
<plurals name="state_focus_remaining">
@ -149,4 +157,11 @@
<string name="breakTime">Break Time</string>
<string name="repeats">Number of Repeats</string>
<!-- Session Recap -->
<string name="congrats">"Congratulations! You studied: %s"</string>
<string name="how_did_it_go">How did it go?</string>
<string name="good">Good</string>
<string name="bad">Bad</string>
</resources>