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.viewmodel)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.material3.core)
|
implementation(libs.material3.core)
|
||||||
|
implementation(libs.material.icons)
|
||||||
implementation(libs.sl4j)
|
implementation(libs.sl4j)
|
||||||
}
|
}
|
||||||
desktopMain.dependencies {
|
desktopMain.dependencies {
|
||||||
|
@ -33,6 +34,7 @@ kotlin {
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.material3.desktop)
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
import kotlinx.datetime.LocalDateTime
|
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||||
import kotlinx.datetime.format
|
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
||||||
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
|
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
|
||||||
import kotlinx.datetime.format.byUnicodePattern
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, FormatStringsInDatetimeFormats::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupAssignmentView(state: GroupAssignmentState) {
|
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||||
val (course, edition) = state.editionCourse
|
val (course, edition) = state.editionCourse
|
||||||
|
@ -28,7 +26,7 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||||
val deadline by state.deadline
|
val deadline by state.deadline
|
||||||
val allFeedback by state.feedback.entities
|
val allFeedback by state.feedback.entities
|
||||||
|
|
||||||
var idx by remember { mutableStateOf(0) }
|
var idx by remember(state) { mutableStateOf(0) }
|
||||||
|
|
||||||
Column(Modifier.padding(10.dp)) {
|
Column(Modifier.padding(10.dp)) {
|
||||||
PaneHeader(name, "group assignment", course, edition)
|
PaneHeader(name, "group assignment", course, edition)
|
||||||
|
@ -50,33 +48,22 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(idx == 0) {
|
if(idx == 0) {
|
||||||
var updTask by remember { mutableStateOf(task) }
|
val updTask = rememberRichTextState()
|
||||||
|
|
||||||
|
LaunchedEffect(task) { updTask.setMarkdown(task) }
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
var showPicker by remember { mutableStateOf(false) }
|
DateTimePicker(deadline, { state.updateDeadline(it) })
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
OutlinedTextField(updTask, { updTask = it }, Modifier.fillMaxWidth().weight(1f), singleLine = false, minLines = 5, label = { Text("Task") })
|
RichTextStyleRow(state = updTask)
|
||||||
CancelSaveRow(updTask != task, { updTask = task }, "Reset", "Update") { state.updateTask(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 {
|
else {
|
||||||
groupFeedback(state, allFeedback[idx - 1].second)
|
groupFeedback(state, allFeedback[idx - 1].second)
|
||||||
|
|
|
@ -4,6 +4,8 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -58,15 +60,19 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||||
state.edition,
|
state.edition,
|
||||||
groups,
|
groups,
|
||||||
idx.groupIdx(),
|
idx.groupIdx(),
|
||||||
{ toggle(it, Panel.Group) }) {
|
{ toggle(it, Panel.Group) },
|
||||||
state.newGroup(it)
|
{ state.newGroup(it) }) { group, name ->
|
||||||
|
state.setGroupName(group, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(0.5f)) {
|
Box(Modifier.weight(0.5f)) {
|
||||||
GroupAssignmentsWidget(
|
GroupAssignmentsWidget(
|
||||||
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) }
|
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) },
|
||||||
) {
|
{ state.newGroupAssignment(it) }) { assignment, title ->
|
||||||
state.newGroupAssignment(it)
|
state.setGroupAssignmentTitle(
|
||||||
|
assignment,
|
||||||
|
title
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,9 +86,13 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(0.5f)) {
|
Box(Modifier.weight(0.5f)) {
|
||||||
AssignmentsWidget(
|
AssignmentsWidget(
|
||||||
state.course, state.edition, solo, idx.soloIdx(), { toggle(it, Panel.Solo) }
|
state.course,
|
||||||
) {
|
state.edition,
|
||||||
state.newSoloAssignment(it)
|
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,
|
course: Course, edition: Edition, header: String, hasNoX: String, addX: String,
|
||||||
data: List<T>, selected: Int?, onSelect: (Int) -> Unit,
|
data: List<T>, selected: Int?, onSelect: (Int) -> Unit,
|
||||||
singleWidget: @Composable (T) -> Unit,
|
singleWidget: @Composable (T) -> Unit,
|
||||||
|
editDialog: @Composable ((current: T, onExit: () -> Unit) -> Unit)? = null,
|
||||||
dialog: @Composable (onExit: () -> Unit) -> Unit
|
dialog: @Composable (onExit: () -> Unit) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
Text(header, style = MaterialTheme.typography.headlineMedium)
|
Text(header, style = MaterialTheme.typography.headlineMedium)
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var current by remember { mutableStateOf<T?>(null) }
|
||||||
|
|
||||||
ListOrEmpty(
|
ListOrEmpty(
|
||||||
data,
|
data,
|
||||||
|
@ -122,11 +134,23 @@ fun <T> EditionSideWidget(
|
||||||
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
||||||
shape = MaterialTheme.shapes.medium
|
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 }
|
if(showDialog) dialog { showDialog = false }
|
||||||
|
editDialog?.let { d ->
|
||||||
|
current?.let { c ->
|
||||||
|
d(c) { current = null }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -135,7 +159,7 @@ fun StudentsWidget(
|
||||||
availableStudents: List<Student>, onImport: (List<Student>) -> Unit,
|
availableStudents: List<Student>, onImport: (List<Student>) -> Unit,
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||||
) = EditionSideWidget(
|
) = 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)) }
|
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||||
) { onExit ->
|
) { onExit ->
|
||||||
StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd)
|
StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd)
|
||||||
|
@ -242,10 +266,11 @@ fun StudentDialog(
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupsWidget(
|
fun GroupsWidget(
|
||||||
course: Course, edition: Edition, groups: List<Group>, selected: Int?, onSelect: (Int) -> Unit,
|
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(
|
) = EditionSideWidget(
|
||||||
course, edition, "Group list", "groups", "a group", groups, selected, onSelect,
|
course, edition, "Group list (${groups.size})", "groups", "a group", groups, selected, onSelect,
|
||||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
{ Text(it.name, Modifier.padding(5.dp)) },
|
||||||
|
{ current, onExit -> AddStringDialog("Group name", groups.map { it.name }, onExit, current.name) { onUpdate(current, it) } }
|
||||||
) { onExit ->
|
) { onExit ->
|
||||||
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
||||||
}
|
}
|
||||||
|
@ -253,10 +278,11 @@ fun GroupsWidget(
|
||||||
@Composable
|
@Composable
|
||||||
fun AssignmentsWidget(
|
fun AssignmentsWidget(
|
||||||
course: Course, edition: Edition, assignments: List<SoloAssignment>, selected: Int?,
|
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(
|
) = EditionSideWidget(
|
||||||
course, edition, "Assignment list", "assignments", "an assignment", assignments, selected, onSelect,
|
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 ->
|
) { onExit ->
|
||||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||||
}
|
}
|
||||||
|
@ -264,10 +290,11 @@ fun AssignmentsWidget(
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupAssignmentsWidget(
|
fun GroupAssignmentsWidget(
|
||||||
course: Course, edition: Edition, assignments: List<GroupAssignment>, selected: Int?,
|
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(
|
) = EditionSideWidget(
|
||||||
course, edition, "Group assignment list", "group assignments", "an assignment", assignments, selected, onSelect,
|
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 ->
|
) { onExit ->
|
||||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
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) {
|
fun StudentView(state: StudentState) {
|
||||||
val groups by state.groups.entities
|
val groups by state.groups.entities
|
||||||
val courses by state.courseEditions.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)) {
|
Column(Modifier.padding(10.dp)) {
|
||||||
PaneHeader(state.student.name, "student", state.editionCourse)
|
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.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogWindow
|
import androidx.compose.ui.window.*
|
||||||
import androidx.compose.ui.window.WindowPosition
|
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
|
||||||
import com.jaytux.grader.data.Course
|
import com.jaytux.grader.data.Course
|
||||||
import com.jaytux.grader.data.Edition
|
import com.jaytux.grader.data.Edition
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
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
|
@Composable
|
||||||
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
||||||
|
@ -63,13 +66,13 @@ fun <T> TabLayout(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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,
|
onCloseRequest = onClose,
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
) {
|
) {
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember(current) { mutableStateOf(current) }
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
||||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
|
@ -198,7 +201,7 @@ fun AutocompleteLineField(
|
||||||
val (lineno, lineStart) = posToLine(pos)
|
val (lineno, lineStart) = posToLine(pos)
|
||||||
|
|
||||||
lines[lineno] = str
|
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 = {
|
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.*
|
||||||
import com.jaytux.grader.data.EditionStudents.editionId
|
import com.jaytux.grader.data.EditionStudents.editionId
|
||||||
import com.jaytux.grader.data.EditionStudents.studentId
|
import com.jaytux.grader.data.EditionStudents.studentId
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.*
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.TimeZone
|
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.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
fun <T> MutableState<T>.immutable(): State<T> = this
|
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()))
|
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()
|
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) {
|
fun newSoloAssignment(name: String) {
|
||||||
transaction {
|
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()
|
solo.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun setSoloAssignmentTitle(assignment: SoloAssignment, title: String) {
|
||||||
|
transaction {
|
||||||
|
assignment.name = title
|
||||||
|
}
|
||||||
|
solo.refresh()
|
||||||
|
}
|
||||||
fun newGroupAssignment(name: String) {
|
fun newGroupAssignment(name: String) {
|
||||||
transaction {
|
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()
|
groupAs.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun setGroupAssignmentTitle(assignment: GroupAssignment, title: String) {
|
||||||
|
transaction {
|
||||||
|
assignment.name = title
|
||||||
|
}
|
||||||
|
groupAs.refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StudentState(val student: Student, edition: Edition) {
|
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 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 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 {
|
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 }
|
(e1, c1), (e2, c2) -> c1.name.compareTo(c2.name).let { if(it == 0) e1.name.compareTo(e2.name) else it }
|
||||||
}.toList() }
|
}.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) {
|
fun update(f: Student.() -> Unit) {
|
||||||
transaction {
|
transaction {
|
||||||
student.f()
|
student.f()
|
||||||
|
@ -263,8 +327,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||||
_task.value = t
|
_task.value = t
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDeadline(instant: Long) {
|
fun updateDeadline(d: LocalDateTime) {
|
||||||
val d = Instant.fromEpochMilliseconds(instant).toLocalDateTime(TimeZone.currentSystemDefault())
|
|
||||||
transaction {
|
transaction {
|
||||||
assignment.deadline = d
|
assignment.deadline = d
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ exposed = "0.59.0"
|
||||||
material3 = "1.7.3"
|
material3 = "1.7.3"
|
||||||
ui-android = "1.7.8"
|
ui-android = "1.7.8"
|
||||||
foundation-layout-android = "1.7.8"
|
foundation-layout-android = "1.7.8"
|
||||||
|
rtf = "1.0.0-rc11"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
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" }
|
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
||||||
material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" }
|
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" }
|
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-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" }
|
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]
|
[plugins]
|
||||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
|
|
Loading…
Reference in New Issue