Slight UI updates

This commit is contained in:
jay-tux 2025-02-25 10:01:53 +01:00
parent 054970bb79
commit fbc450e0ee
Signed by: jay-tux
GPG Key ID: 84302006B056926E
8 changed files with 464 additions and 62 deletions

View File

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

View File

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

View File

@ -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 <T> EditionSideWidget(
course: Course, edition: Edition, header: String, hasNoX: String, addX: String,
data: List<T>, 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<T?>(null) }
ListOrEmpty(
data,
@ -122,11 +134,23 @@ fun <T> 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<Student>, onImport: (List<Student>) -> 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<Group>, 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<SoloAssignment>, 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<GroupAssignment>, 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) }
}

View File

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

View File

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

View File

@ -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 <T> TabLayout(
}
@Composable
fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, onSave: (String) -> Unit) = DialogWindow(
fun AddStringDialog(label: String, taken: List<String>, 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),
// )
// }
}
}

View File

@ -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 <T> MutableState<T>.immutable(): State<T> = this
fun <T> SizedIterable<T>.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<EntityID<UUID>, 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
}

View File

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