From fbc450e0eeab0778291673f1a3ccde85789ba141 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 25 Feb 2025 10:01:53 +0100 Subject: [PATCH] Slight UI updates --- composeApp/build.gradle.kts | 2 + .../com/jaytux/grader/ui/Assignments.kt | 51 ++-- .../kotlin/com/jaytux/grader/ui/Editions.kt | 61 +++-- .../kotlin/com/jaytux/grader/ui/RichText.kt | 228 ++++++++++++++++++ .../kotlin/com/jaytux/grader/ui/Views.kt | 4 + .../kotlin/com/jaytux/grader/ui/Widgets.kt | 100 +++++++- .../com/jaytux/grader/viewmodel/DbState.kt | 77 +++++- gradle/libs.versions.toml | 3 + 8 files changed, 464 insertions(+), 62 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index a6573c5..ecfbd76 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.material3.core) + implementation(libs.material.icons) implementation(libs.sl4j) } desktopMain.dependencies { @@ -33,6 +34,7 @@ kotlin { implementation(libs.exposed.kotlin.datetime) implementation(libs.sqlite) implementation(libs.material3.desktop) + implementation(libs.rtfield) } } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt index 19fbd4e..9ff78f8 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -12,14 +12,12 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties import com.jaytux.grader.viewmodel.GroupAssignmentState -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.format -import kotlinx.datetime.format.FormatStringsInDatetimeFormats -import kotlinx.datetime.format.byUnicodePattern +import com.mohamedrejeb.richeditor.model.rememberRichTextState +import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor +import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor -@OptIn(ExperimentalMaterial3Api::class, FormatStringsInDatetimeFormats::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupAssignmentView(state: GroupAssignmentState) { val (course, edition) = state.editionCourse @@ -28,7 +26,7 @@ fun GroupAssignmentView(state: GroupAssignmentState) { val deadline by state.deadline val allFeedback by state.feedback.entities - var idx by remember { mutableStateOf(0) } + var idx by remember(state) { mutableStateOf(0) } Column(Modifier.padding(10.dp)) { PaneHeader(name, "group assignment", course, edition) @@ -50,33 +48,22 @@ fun GroupAssignmentView(state: GroupAssignmentState) { } if(idx == 0) { - var updTask by remember { mutableStateOf(task) } + val updTask = rememberRichTextState() + + LaunchedEffect(task) { updTask.setMarkdown(task) } + Row { - var showPicker by remember { mutableStateOf(false) } - val dateState = rememberDatePickerState() - - Text("Deadline: ${deadline.format(LocalDateTime.Format { byUnicodePattern("dd/MM/yyyy - HH:mm") })}", Modifier.align(Alignment.CenterVertically)) - Spacer(Modifier.width(10.dp)) - Button({ showPicker = true }) { Text("Change") } - - if(showPicker) DatePickerDialog( - { showPicker = false }, - { Button({ showPicker = false; dateState.selectedDateMillis?.let { state.updateDeadline(it) } }) { Text("Set deadline") } }, - Modifier, - { Button({ showPicker = false }) { Text("Cancel") } }, - shape = MaterialTheme.shapes.medium, - tonalElevation = 10.dp, - colors = DatePickerDefaults.colors(), - properties = DialogProperties() - ) { - DatePicker( - dateState, - Modifier.fillMaxWidth().padding(10.dp), - ) - } + DateTimePicker(deadline, { state.updateDeadline(it) }) } - OutlinedTextField(updTask, { updTask = it }, Modifier.fillMaxWidth().weight(1f), singleLine = false, minLines = 5, label = { Text("Task") }) - CancelSaveRow(updTask != task, { updTask = task }, "Reset", "Update") { state.updateTask(updTask) } + RichTextStyleRow(state = updTask) + OutlinedRichTextEditor( + state = updTask, + modifier = Modifier.fillMaxWidth().weight(1f), + singleLine = false, + minLines = 5, + label = { Text("Task") } + ) + CancelSaveRow(true, { updTask.setMarkdown(task) }, "Reset", "Update") { state.updateTask(updTask.toMarkdown()) } } else { groupFeedback(state, allFeedback[idx - 1].second) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt index 35b9fc9..3cf0a8c 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -58,15 +60,19 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { state.edition, groups, idx.groupIdx(), - { toggle(it, Panel.Group) }) { - state.newGroup(it) + { toggle(it, Panel.Group) }, + { state.newGroup(it) }) { group, name -> + state.setGroupName(group, name) } } Box(Modifier.weight(0.5f)) { GroupAssignmentsWidget( - state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) } - ) { - state.newGroupAssignment(it) + state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) }, + { state.newGroupAssignment(it) }) { assignment, title -> + state.setGroupAssignmentTitle( + assignment, + title + ) } } } else { @@ -80,9 +86,13 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { } Box(Modifier.weight(0.5f)) { AssignmentsWidget( - state.course, state.edition, solo, idx.soloIdx(), { toggle(it, Panel.Solo) } - ) { - state.newSoloAssignment(it) + state.course, + state.edition, + solo, + idx.soloIdx(), + { toggle(it, Panel.Solo) }, + { state.newSoloAssignment(it) }) { assignment, title -> + state.setSoloAssignmentTitle(assignment, title) } } } @@ -106,10 +116,12 @@ fun EditionSideWidget( course: Course, edition: Edition, header: String, hasNoX: String, addX: String, data: List, selected: Int?, onSelect: (Int) -> Unit, singleWidget: @Composable (T) -> Unit, + editDialog: @Composable ((current: T, onExit: () -> Unit) -> Unit)? = null, dialog: @Composable (onExit: () -> Unit) -> Unit ) = Column(Modifier.padding(10.dp)) { Text(header, style = MaterialTheme.typography.headlineMedium) var showDialog by remember { mutableStateOf(false) } + var current by remember { mutableStateOf(null) } ListOrEmpty( data, @@ -122,11 +134,23 @@ fun EditionSideWidget( tonalElevation = if (selected == idx) 50.dp else 0.dp, shape = MaterialTheme.shapes.medium ) { - singleWidget(it) + Row { + Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { singleWidget(it) } + editDialog?.let { _ -> + IconButton({ current = it }, Modifier.align(Alignment.CenterVertically)) { + Icon(Icons.Default.Edit, "Edit") + } + } + } } } if(showDialog) dialog { showDialog = false } + editDialog?.let { d -> + current?.let { c -> + d(c) { current = null } + } + } } @Composable @@ -135,7 +159,7 @@ fun StudentsWidget( availableStudents: List, onImport: (List) -> Unit, onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit ) = EditionSideWidget( - course, edition, "Student list", "students", "a student", students, selected, onSelect, + course, edition, "Student list (${students.size})", "students", "a student", students, selected, onSelect, { Text(it.name, Modifier.padding(5.dp)) } ) { onExit -> StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd) @@ -242,10 +266,11 @@ fun StudentDialog( @Composable fun GroupsWidget( course: Course, edition: Edition, groups: List, selected: Int?, onSelect: (Int) -> Unit, - onAdd: (name: String) -> Unit + onAdd: (name: String) -> Unit, onUpdate: (Group, String) -> Unit ) = EditionSideWidget( - course, edition, "Group list", "groups", "a group", groups, selected, onSelect, - { Text(it.name, Modifier.padding(5.dp)) } + course, edition, "Group list (${groups.size})", "groups", "a group", groups, selected, onSelect, + { Text(it.name, Modifier.padding(5.dp)) }, + { current, onExit -> AddStringDialog("Group name", groups.map { it.name }, onExit, current.name) { onUpdate(current, it) } } ) { onExit -> AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) } } @@ -253,10 +278,11 @@ fun GroupsWidget( @Composable fun AssignmentsWidget( course: Course, edition: Edition, assignments: List, selected: Int?, - onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit + onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit, onUpdate: (SoloAssignment, String) -> Unit ) = EditionSideWidget( course, edition, "Assignment list", "assignments", "an assignment", assignments, selected, onSelect, - { Text(it.name, Modifier.padding(5.dp)) } + { Text(it.name, Modifier.padding(5.dp)) }, + { current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } } ) { onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) } } @@ -264,10 +290,11 @@ fun AssignmentsWidget( @Composable fun GroupAssignmentsWidget( course: Course, edition: Edition, assignments: List, selected: Int?, - onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit + onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit, onUpdate: (GroupAssignment, String) -> Unit ) = EditionSideWidget( course, edition, "Group assignment list", "group assignments", "an assignment", assignments, selected, onSelect, - { Text(it.name, Modifier.padding(5.dp)) } + { Text(it.name, Modifier.padding(5.dp)) }, + { current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } } ) { onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt new file mode 100644 index 0000000..6c59e62 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt @@ -0,0 +1,228 @@ +package com.jaytux.grader.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi +import com.mohamedrejeb.richeditor.model.RichTextState + +@OptIn(ExperimentalRichTextApi::class) +@Composable +fun RichTextStyleRow( + modifier: Modifier = Modifier, + state: RichTextState, +) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + fontWeight = FontWeight.Bold + ) + ) + }, + isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold, + icon = Icons.Outlined.FormatBold + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + fontStyle = FontStyle.Italic + ) + ) + }, + isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic, + icon = Icons.Outlined.FormatItalic + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + textDecoration = TextDecoration.Underline + ) + ) + }, + isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true, + icon = Icons.Outlined.FormatUnderlined + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + textDecoration = TextDecoration.LineThrough + ) + ) + }, + isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true, + icon = Icons.Outlined.FormatStrikethrough + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + fontSize = 28.sp + ) + ) + }, + isSelected = state.currentSpanStyle.fontSize == 28.sp, + icon = Icons.Outlined.FormatSize + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + color = Color.Red + ) + ) + }, + isSelected = state.currentSpanStyle.color == Color.Red, + icon = Icons.Filled.Circle, + tint = Color.Red + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleSpanStyle( + SpanStyle( + background = Color.Yellow + ) + ) + }, + isSelected = state.currentSpanStyle.background == Color.Yellow, + icon = Icons.Outlined.Circle, + tint = Color.Yellow + ) + } + + item { + Box( + Modifier + .height(24.dp) + .width(1.dp) + .background(Color(0xFF393B3D)) + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleUnorderedList() + }, + isSelected = state.isUnorderedList, + icon = Icons.AutoMirrored.Outlined.FormatListBulleted, + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleOrderedList() + }, + isSelected = state.isOrderedList, + icon = Icons.Outlined.FormatListNumbered, + ) + } + + item { + Box( + Modifier + .height(24.dp) + .width(1.dp) + .background(Color(0xFF393B3D)) + ) + } + + item { + RichTextStyleButton( + onClick = { + state.toggleCodeSpan() + }, + isSelected = state.isCodeSpan, + icon = Icons.Outlined.Code, + ) + } + } +} + +@Composable +fun RichTextStyleButton( + onClick: () -> Unit, + icon: ImageVector, + tint: Color? = null, + isSelected: Boolean = false, +) { + IconButton( + modifier = Modifier + // Workaround to prevent the rich editor + // from losing focus when clicking on the button + // (Happens only on Desktop) + .focusProperties { canFocus = false }, + onClick = onClick, + colors = IconButtonDefaults.iconButtonColors( + contentColor = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + }, + ), + ) { + Icon( + icon, + contentDescription = icon.name, + tint = tint ?: LocalContentColor.current, + modifier = Modifier + .background( + color = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + }, + shape = CircleShape + ) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt index cf631b7..b2944ca 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt @@ -23,6 +23,10 @@ import com.jaytux.grader.viewmodel.StudentState fun StudentView(state: StudentState) { val groups by state.groups.entities val courses by state.courseEditions.entities + val groupGrades by state.groupGrades.entities + val soloGrades by state.soloGrades.entities + + // TODO: incorporate grades into UI Column(Modifier.padding(10.dp)) { PaneHeader(state.student.name, "student", state.editionCourse) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index 2f722c9..28f4688 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -24,13 +24,16 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogWindow -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberDialogState +import androidx.compose.ui.window.* import com.jaytux.grader.data.Course import com.jaytux.grader.data.Edition import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.datetime.* +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.datetime.format.byUnicodePattern +import java.util.* @Composable fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) { @@ -63,13 +66,13 @@ fun TabLayout( } @Composable -fun AddStringDialog(label: String, taken: List, onClose: () -> Unit, onSave: (String) -> Unit) = DialogWindow( +fun AddStringDialog(label: String, taken: List, onClose: () -> Unit, current: String = "", onSave: (String) -> Unit) = DialogWindow( onCloseRequest = onClose, state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center)) ) { Surface(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize().padding(10.dp)) { - var name by remember { mutableStateOf("") } + var name by remember(current) { mutableStateOf(current) } Column(Modifier.align(Alignment.Center)) { androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken) CancelSaveRow(name.isNotBlank() && name !in taken, onClose) { @@ -198,7 +201,7 @@ fun AutocompleteLineField( val (lineno, lineStart) = posToLine(pos) lines[lineno] = str - onValueChange(value.copy(text = lines.joinToString("\n"), selection = TextRange(lineStart + str.length))) + onValueChange(value.copy(text = lines.joinToString("\n"), selection = TextRange(lineStart + str.length + 1))) } val currentLine = { @@ -254,4 +257,89 @@ fun AutocompleteLineField( } } } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateTimePicker( + value: LocalDateTime, + onPick: (LocalDateTime) -> Unit, + formatter: (LocalDateTime) -> String = { java.text.DateFormat.getDateTimeInstance().format(Date.from(it.toInstant(TimeZone.currentSystemDefault()).toJavaInstant())) }, + modifier: Modifier = Modifier, +) { + var showPicker by remember { mutableStateOf(false) } + + Row(modifier) { + Text( + formatter(value), + Modifier.align(Alignment.CenterVertically) + ) + Spacer(Modifier.width(10.dp)) + Button({ showPicker = true }) { Text("Change") } + + if (showPicker) { + val dateState = rememberDatePickerState(value.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()) + val timeState = rememberTimePickerState(value.hour, value.minute) + + Dialog( + { showPicker = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + shape = MaterialTheme.shapes.extraLarge, tonalElevation = 6.dp, + modifier = Modifier.width(800.dp).height(600.dp) + ) { + val colors = TimePickerDefaults.colors( + selectorColor = MaterialTheme.colorScheme.primary, + timeSelectorSelectedContainerColor = MaterialTheme.colorScheme.primary, + timeSelectorSelectedContentColor = MaterialTheme.colorScheme.onPrimary, + clockDialSelectedContentColor = MaterialTheme.colorScheme.onPrimary, + ) // the colors are fucked, and I don't get why :( + + Column(Modifier.padding(10.dp)) { + Row { + DatePicker( + dateState, + Modifier.padding(10.dp).weight(0.5f), + ) + TimePicker( + timeState, + Modifier.weight(0.5f).align(Alignment.CenterVertically), + layoutType = TimePickerLayoutType.Vertical, + colors = colors + ) + } + CancelSaveRow(true, { showPicker = false }) { + val date = (dateState.selectedDateMillis?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault()) } ?: value).date + val time = LocalTime(timeState.hour, timeState.minute) + + onPick(LocalDateTime(date, time)) + showPicker = false + } + } + } + } + } + + +// DatePickerDialog( +// { showPicker = false }, +// { +// Button({ +// showPicker = false; dateState.selectedDateMillis?.let { state.updateDeadline(it) } +// }) { Text("Set deadline") } +// }, +// Modifier, +// { Button({ showPicker = false }) { Text("Cancel") } }, +// shape = MaterialTheme.shapes.medium, +// tonalElevation = 10.dp, +// colors = DatePickerDefaults.colors(), +// properties = DialogProperties() +// ) { +// DatePicker( +// dateState, +// Modifier.fillMaxWidth().padding(10.dp), +// ) +// } + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index b452dfc..2a8fa32 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -6,13 +6,13 @@ import androidx.compose.runtime.mutableStateOf import com.jaytux.grader.data.* import com.jaytux.grader.data.EditionStudents.editionId import com.jaytux.grader.data.EditionStudents.studentId -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.* import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime +import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction +import java.util.* fun MutableState.immutable(): State = this fun SizedIterable.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray())) @@ -102,28 +102,92 @@ class EditionState(val edition: Edition) { groups.refresh() } } + fun setGroupName(group: Group, name: String) { + transaction { + group.name = name + } + groups.refresh() + } + + private fun now(): LocalDateTime { + val instant = Instant.fromEpochMilliseconds(System.currentTimeMillis()) + return instant.toLocalDateTime(TimeZone.currentSystemDefault()) + } fun newSoloAssignment(name: String) { transaction { - SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" } + SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() } solo.refresh() } } + fun setSoloAssignmentTitle(assignment: SoloAssignment, title: String) { + transaction { + assignment.name = title + } + solo.refresh() + } fun newGroupAssignment(name: String) { transaction { - GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" } + GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() } groupAs.refresh() } } + fun setGroupAssignmentTitle(assignment: GroupAssignment, title: String) { + transaction { + assignment.name = title + } + groupAs.refresh() + } } class StudentState(val student: Student, edition: Edition) { + data class LocalGroupGrade(val groupName: String, val assignmentName: String, val groupGrade: String?, val indivGrade: String?) + data class LocalSoloGrade(val assignmentName: String, val grade: String) + val editionCourse = transaction { edition.course to edition } val groups = RawDbState { student.groups.sortAsc(Groups.name).map { it to (it.edition.course.name to it.edition.name) }.toList() } val courseEditions = RawDbState { student.courses.map{ it to it.course }.sortedWith { (e1, c1), (e2, c2) -> c1.name.compareTo(c2.name).let { if(it == 0) e1.name.compareTo(e2.name) else it } }.toList() } + val groupGrades = RawDbState { + val groupsForEdition = Group.find { + (Groups.editionId eq edition.id) and (Groups.id inList student.groups.map { it.id }) + }.associate { it.id to it.name } + + val asGroup = (GroupAssignments innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { + GroupFeedbacks.groupId inList groupsForEdition.keys.toList() + }.map { it[GroupFeedbacks.groupAssignmentId] to it } + + val asIndividual = (GroupAssignments innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { + IndividualFeedbacks.studentId eq student.id + }.map { it[IndividualFeedbacks.groupAssignmentId] to it } + + val res = mutableMapOf, LocalGroupGrade>() + asGroup.forEach { + val (gAId, gRow) = it + + res[gAId] = LocalGroupGrade( + gRow[Groups.name], gRow[GroupAssignments.name], gRow[GroupFeedbacks.grade], null + ) + } + + asIndividual.forEach { + val (gAId, iRow) = it + + val og = res[gAId] ?: LocalGroupGrade(iRow[Groups.name], iRow[GroupAssignments.name], null, null) + res[gAId] = og.copy(indivGrade = iRow[IndividualFeedbacks.grade]) + } + + res.values.toList() + } + + val soloGrades = RawDbState { + (SoloAssignments innerJoin SoloFeedbacks).selectAll().where { + SoloFeedbacks.studentId eq student.id + }.map { LocalSoloGrade(it[SoloAssignments.name], it[SoloFeedbacks.grade]) }.toList() + } + fun update(f: Student.() -> Unit) { transaction { student.f() @@ -263,8 +327,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { _task.value = t } - fun updateDeadline(instant: Long) { - val d = Instant.fromEpochMilliseconds(instant).toLocalDateTime(TimeZone.currentSystemDefault()) + fun updateDeadline(d: LocalDateTime) { transaction { assignment.deadline = d } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1adde27..46d35c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ exposed = "0.59.0" material3 = "1.7.3" ui-android = "1.7.8" foundation-layout-android = "1.7.8" +rtf = "1.0.0-rc11" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -24,8 +25,10 @@ sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" } sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" } material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" } material3-desktop = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" } +material-icons = { group = "org.jetbrains.compose.material", name = "material-icons-extended", version.ref = "material3" } androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" } androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" } +rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" } [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }