Slight UI updates
This commit is contained in:
parent
054970bb79
commit
fbc450e0ee
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in New Issue