UI fixes and additions
This commit is contained in:
parent
d0ddd54710
commit
b69b46afee
|
@ -1,5 +1,11 @@
|
||||||
package com.jaytux.grader
|
package com.jaytux.grader
|
||||||
|
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
fun String.maxN(n: Int): String {
|
fun String.maxN(n: Int): String {
|
||||||
return if (this.length > n) {
|
return if (this.length > n) {
|
||||||
this.substring(0, n - 3) + "..."
|
this.substring(0, n - 3) + "..."
|
||||||
|
@ -7,3 +13,11 @@ fun String.maxN(n: Int): String {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RichTextState.toClipboard(clip: ClipboardManager) {
|
||||||
|
clip.setText(AnnotatedString(this.toMarkdown()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RichTextState.loadClipboard(clip: ClipboardManager, scope: CoroutineScope) {
|
||||||
|
scope.launch { setMarkdown(clip.getText()?.text ?: "") }
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ 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 com.jaytux.grader.viewmodel.GroupAssignmentState
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
|
import com.jaytux.grader.viewmodel.SoloAssignmentState
|
||||||
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||||
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
||||||
|
|
||||||
|
@ -145,3 +146,93 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SoloAssignmentView(state: SoloAssignmentState) {
|
||||||
|
val name by state.name
|
||||||
|
val (course, edition) = state.editionCourse
|
||||||
|
val task by state.task
|
||||||
|
val deadline by state.deadline
|
||||||
|
val suggestions by state.autofill.entities
|
||||||
|
val grades by state.feedback.entities
|
||||||
|
|
||||||
|
var idx by remember(state) { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
PaneHeader(name, "individual assignment", course, edition)
|
||||||
|
Row {
|
||||||
|
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||||
|
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
|
||||||
|
item {
|
||||||
|
Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { idx = 0 },
|
||||||
|
tonalElevation = if (idx == 0) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(grades.toList()) { i, (student, _) ->
|
||||||
|
Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
||||||
|
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text(student.name, Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||||
|
if (idx == 0) {
|
||||||
|
val updTask = rememberRichTextState()
|
||||||
|
|
||||||
|
LaunchedEffect(task) { updTask.setMarkdown(task) }
|
||||||
|
|
||||||
|
Row {
|
||||||
|
DateTimePicker(deadline, { state.updateDeadline(it) })
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
val (student, fg) = grades[idx - 1]
|
||||||
|
var sGrade by remember { mutableStateOf(fg?.grade ?: "") }
|
||||||
|
var sMsg by remember { mutableStateOf(TextFieldValue(fg?.feedback ?: "")) }
|
||||||
|
Row {
|
||||||
|
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||||
|
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
|
||||||
|
Spacer(Modifier.weight(0.6f))
|
||||||
|
Button(
|
||||||
|
{ state.upsertFeedback(student, sMsg.text, sGrade) },
|
||||||
|
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||||
|
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutocompleteLineField(
|
||||||
|
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||||
|
) { filter ->
|
||||||
|
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
||||||
val data by state.courses.entities
|
val data by state.courses.entities
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(Modifier.padding(15.dp)) {
|
||||||
ListOrEmpty(
|
ListOrEmpty(
|
||||||
data,
|
data,
|
||||||
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
||||||
|
@ -36,6 +37,7 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
||||||
) { _, it ->
|
) { _, it ->
|
||||||
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,6 @@ 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.Delete
|
|
||||||
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
|
||||||
|
@ -16,185 +13,264 @@ import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import com.jaytux.grader.data.*
|
import com.jaytux.grader.data.*
|
||||||
import com.jaytux.grader.viewmodel.EditionState
|
import com.jaytux.grader.viewmodel.*
|
||||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
|
||||||
import com.jaytux.grader.viewmodel.GroupState
|
|
||||||
import com.jaytux.grader.viewmodel.StudentState
|
|
||||||
|
|
||||||
enum class Panel { Student, Group, Solo, GroupAs }
|
enum class OpenPanel(val tabName: String) {
|
||||||
data class Current(val p: Panel, val i: Int)
|
Student("Students"), Group("Groups"), Assignment("Assignments")
|
||||||
fun Current?.studentIdx() = this?.let { if(p == Panel.Student) i else null }
|
}
|
||||||
fun Current?.groupIdx() = this?.let { if(p == Panel.Group) i else null }
|
|
||||||
fun Current?.soloIdx() = this?.let { if(p == Panel.Solo) i else null }
|
data class Navigators(
|
||||||
fun Current?.groupAsIdx() = this?.let { if(p == Panel.GroupAs) i else null }
|
val student: (Student) -> Unit,
|
||||||
|
val group: (Group) -> Unit,
|
||||||
|
val assignment: (Assignment) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||||
var isGroup by remember { mutableStateOf(false) }
|
val course = state.course; val edition = state.edition
|
||||||
var idx by remember { mutableStateOf<Current?>(null) }
|
|
||||||
|
|
||||||
val students by state.students.entities
|
val students by state.students.entities
|
||||||
|
val availableStudents by state.availableStudents.entities
|
||||||
val groups by state.groups.entities
|
val groups by state.groups.entities
|
||||||
val solo by state.solo.entities
|
val solo by state.solo.entities
|
||||||
val groupAs by state.groupAs.entities
|
val groupAs by state.groupAs.entities
|
||||||
val available by state.availableStudents.entities
|
val mergedAssignments by remember(solo, groupAs) {
|
||||||
|
mutableStateOf(Assignment.merge(groupAs, solo))
|
||||||
val toggle = { i: Int, p: Panel ->
|
|
||||||
idx = if(idx?.p == p && idx?.i == i) null else Current(p, i)
|
|
||||||
}
|
}
|
||||||
|
var selected by remember { mutableStateOf(-1) }
|
||||||
|
var tab by remember { mutableStateOf(OpenPanel.Assignment) }
|
||||||
|
|
||||||
|
val navs = Navigators(
|
||||||
|
student = { tab = OpenPanel.Student; selected = students.indexOfFirst { s -> s.id == it.id } },
|
||||||
|
group = { tab = OpenPanel.Group; selected = groups.indexOfFirst { g -> g.id == it.id } },
|
||||||
|
assignment = { tab = OpenPanel.Assignment; selected = mergedAssignments.indexOfFirst { a -> a.id() == it.id() } }
|
||||||
|
)
|
||||||
|
|
||||||
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
||||||
TabLayout(
|
TabLayout(
|
||||||
listOf("Students", "Groups"),
|
OpenPanel.entries,
|
||||||
if (isGroup) 1 else 0,
|
tab.ordinal,
|
||||||
{ isGroup = it == 1 },
|
{ tab = OpenPanel.entries[it]; selected = -1 },
|
||||||
{ Text(it) }
|
{ Text(it.tabName) }
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize()) {
|
when(tab) {
|
||||||
if (isGroup) {
|
OpenPanel.Student -> StudentPanel(
|
||||||
Box(Modifier.weight(0.5f)) {
|
course, edition, students, availableStudents, selected,
|
||||||
GroupsWidget(
|
{ selected = it },
|
||||||
state.course,
|
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
||||||
state.edition,
|
{ students -> state.addToCourse(students) },
|
||||||
groups,
|
{ s, name -> state.setStudentName(s, name) }
|
||||||
idx.groupIdx(),
|
) { s -> state.delete(s) }
|
||||||
{ toggle(it, Panel.Group) },
|
|
||||||
{ state.delete(it) },
|
OpenPanel.Group -> GroupPanel(
|
||||||
{ state.newGroup(it) }) { group, name ->
|
course, edition, groups, selected,
|
||||||
state.setGroupName(group, name)
|
{ selected = it },
|
||||||
}
|
{ name -> state.newGroup(name) },
|
||||||
}
|
{ g, name -> state.setGroupName(g, name) }
|
||||||
Box(Modifier.weight(0.5f)) {
|
) { g -> state.delete(g) }
|
||||||
GroupAssignmentsWidget(
|
|
||||||
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) },
|
OpenPanel.Assignment -> AssignmentPanel(
|
||||||
{ state.delete(it) },
|
course, edition, mergedAssignments, selected,
|
||||||
{ state.newGroupAssignment(it) }) { assignment, title ->
|
{ selected = it },
|
||||||
state.setGroupAssignmentTitle(
|
{ type, name -> state.newAssignment(type, name) },
|
||||||
assignment,
|
{ a, name -> state.setAssignmentTitle(a, name) }
|
||||||
title
|
) { a -> state.delete(a) }
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box(Modifier.weight(0.5f)) {
|
|
||||||
StudentsWidget(
|
|
||||||
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) },
|
|
||||||
available, { state.addToCourse(it) },
|
|
||||||
{ state.delete(it) },
|
|
||||||
) { name, note, contact, addToEdition ->
|
|
||||||
state.newStudent(name, contact, note, addToEdition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(Modifier.weight(0.5f)) {
|
|
||||||
AssignmentsWidget(
|
|
||||||
state.course,
|
|
||||||
state.edition,
|
|
||||||
solo,
|
|
||||||
idx.soloIdx(),
|
|
||||||
{ toggle(it, Panel.Solo) },
|
|
||||||
{ state.delete(it) },
|
|
||||||
{ state.newSoloAssignment(it) }) { assignment, title ->
|
|
||||||
state.setSoloAssignmentTitle(assignment, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.weight(0.75f)) {
|
Box(Modifier.weight(0.75f)) {
|
||||||
idx?.let { i ->
|
if(selected != -1) {
|
||||||
when(i.p) {
|
when(tab) {
|
||||||
Panel.Student -> StudentView(StudentState(students[i.i], state.edition))
|
OpenPanel.Student -> StudentView(StudentState(students[selected], edition), navs)
|
||||||
Panel.Group -> GroupView(GroupState(groups[i.i]))
|
OpenPanel.Group -> GroupView(GroupState(groups[selected]), navs)
|
||||||
Panel.GroupAs -> GroupAssignmentView(GroupAssignmentState(groupAs[i.i]))
|
OpenPanel.Assignment -> {
|
||||||
else -> {}
|
when(val a = mergedAssignments[selected]) {
|
||||||
|
is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
|
||||||
|
is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> EditionSideWidget(
|
fun StudentPanel(
|
||||||
course: Course, edition: Edition, header: String, hasNoX: String, addX: String,
|
course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
||||||
data: List<T>, selected: Int?, onSelect: (Int) -> Unit,
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
singleWidget: @Composable (T) -> Unit,
|
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
||||||
editDialog: @Composable ((current: T, onExit: () -> Unit) -> Unit)? = null,
|
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student) -> Unit
|
||||||
deleter: ((T) -> Unit)? = null,
|
|
||||||
dialog: @Composable (onExit: () -> Unit) -> Unit
|
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
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) }
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
var deleting by remember { mutableStateOf<T?>(null) }
|
var editing by remember { mutableStateOf(-1) }
|
||||||
|
|
||||||
|
Text("Student list (${students.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
ListOrEmpty(
|
ListOrEmpty(
|
||||||
data,
|
students,
|
||||||
{ Text("Course ${course.name} (edition ${edition.name})\nhas no $hasNoX yet.", Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center) },
|
{ Text(
|
||||||
{ Text("Add $addX") },
|
"Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
||||||
|
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
|
) },
|
||||||
|
{ Text("Add a student") },
|
||||||
{ showDialog = true }
|
{ showDialog = true }
|
||||||
) { idx, it ->
|
) { idx, it ->
|
||||||
Surface(
|
SelectEditDeleteRow(
|
||||||
Modifier.fillMaxWidth().clickable { onSelect(idx) },
|
selected == idx,
|
||||||
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
{ onSelect(idx) }, { onSelect(-1) },
|
||||||
shape = MaterialTheme.shapes.medium
|
{ editing = idx }, { deleting = idx }
|
||||||
) {
|
) {
|
||||||
Row {
|
Text(it.name, Modifier.padding(5.dp))
|
||||||
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { singleWidget(it) }
|
|
||||||
editDialog?.let { _ ->
|
|
||||||
IconButton({ current = it }, Modifier.align(Alignment.CenterVertically)) {
|
|
||||||
Icon(Icons.Default.Edit, "Edit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleter?.let { d ->
|
|
||||||
IconButton({ deleting = it }, Modifier.align(Alignment.CenterVertically)) {
|
|
||||||
Icon(Icons.Default.Delete, "Delete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showDialog) dialog { showDialog = false }
|
if(showDialog) {
|
||||||
editDialog?.let { d ->
|
StudentDialog(course, edition, { showDialog = false }, available, onImport, onAdd)
|
||||||
current?.let { c ->
|
}
|
||||||
d(c) { current = null }
|
else if(editing != -1) {
|
||||||
}
|
AddStringDialog("Student name", students.map { it.name }, { editing = -1 }, students[editing].name) {
|
||||||
}
|
onUpdate(students[editing], it)
|
||||||
deleter?.let { d ->
|
|
||||||
deleting?.let { x ->
|
|
||||||
DialogWindow(
|
|
||||||
onCloseRequest = { deleting = null },
|
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
|
||||||
) {
|
|
||||||
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) {
|
|
||||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
|
||||||
Text("You are about to delete $addX.", Modifier.padding(10.dp))
|
|
||||||
singleWidget(x)
|
|
||||||
CancelSaveRow(true, { deleting = null }, "Cancel", "Delete") {
|
|
||||||
d(x)
|
|
||||||
deleting = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(deleting != -1) {
|
||||||
|
ConfirmDeleteDialog(
|
||||||
|
"a student",
|
||||||
|
{ deleting = -1 },
|
||||||
|
{ onDelete(students[deleting]) }
|
||||||
|
) { Text(students[deleting].name) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StudentsWidget(
|
fun GroupPanel(
|
||||||
course: Course, edition: Edition, students: List<Student>, selected: Int?, onSelect: (Int) -> Unit,
|
course: Course, edition: Edition, groups: List<Group>,
|
||||||
availableStudents: List<Student>, onImport: (List<Student>) -> Unit, deleter: (Student) -> Unit,
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group) -> Unit
|
||||||
) = EditionSideWidget(
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
course, edition, "Student list (${students.size})", "students", "a student", students, selected, onSelect,
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
{ Text(it.name, Modifier.padding(5.dp)) },
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
deleter = deleter
|
var editing by remember { mutableStateOf(-1) }
|
||||||
) { onExit ->
|
|
||||||
StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd)
|
Text("Group list (${groups.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
|
ListOrEmpty(
|
||||||
|
groups,
|
||||||
|
{ Text(
|
||||||
|
"Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
||||||
|
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
|
) },
|
||||||
|
{ Text("Add a group") },
|
||||||
|
{ showDialog = true }
|
||||||
|
) { idx, it ->
|
||||||
|
SelectEditDeleteRow(
|
||||||
|
selected == idx,
|
||||||
|
{ onSelect(idx) }, { onSelect(-1) },
|
||||||
|
{ editing = idx }, { deleting = idx }
|
||||||
|
) {
|
||||||
|
Text(it.name, Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showDialog) {
|
||||||
|
AddStringDialog("Group name", groups.map{ it.name }, { showDialog = false }) { onAdd(it) }
|
||||||
|
}
|
||||||
|
else if(editing != -1) {
|
||||||
|
AddStringDialog("Group name", groups.map { it.name }, { editing = -1 }, groups[editing].name) {
|
||||||
|
onUpdate(groups[editing], it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(deleting != -1) {
|
||||||
|
ConfirmDeleteDialog(
|
||||||
|
"a group",
|
||||||
|
{ deleting = -1 },
|
||||||
|
{ onDelete(groups[deleting]) }
|
||||||
|
) { Text(groups[deleting].name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AssignmentPanel(
|
||||||
|
course: Course, edition: Edition, assignments: List<Assignment>,
|
||||||
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
|
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
||||||
|
onDelete: (Assignment) -> Unit
|
||||||
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
|
var editing by remember { mutableStateOf(-1) }
|
||||||
|
|
||||||
|
val dialog: @Composable (String, List<String>, () -> Unit, String, (AssignmentType, String) -> Unit) -> Unit =
|
||||||
|
{ label, taken, onClose, current, onSave ->
|
||||||
|
DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
var name by remember(current) { mutableStateOf(current) }
|
||||||
|
var tab by remember { mutableStateOf(AssignmentType.Solo) }
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
TabLayout(
|
||||||
|
AssignmentType.entries,
|
||||||
|
tab.ordinal,
|
||||||
|
{ tab = AssignmentType.entries[it] },
|
||||||
|
{ Text(it.name) }
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
name,
|
||||||
|
{ name = it },
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
label = { Text(label) },
|
||||||
|
isError = name in taken
|
||||||
|
)
|
||||||
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
|
onSave(tab, name)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Assignment list (${assignments.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
|
ListOrEmpty(
|
||||||
|
assignments,
|
||||||
|
{ Text(
|
||||||
|
"Course ${course.name} (edition ${edition.name})\nhas no assignments yet.",
|
||||||
|
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
|
) },
|
||||||
|
{ Text("Add an assignment") },
|
||||||
|
{ showDialog = true }
|
||||||
|
) { idx, it ->
|
||||||
|
SelectEditDeleteRow(
|
||||||
|
selected == idx,
|
||||||
|
{ onSelect(idx) }, { onSelect(-1) },
|
||||||
|
{ editing = idx }, { deleting = idx }
|
||||||
|
) {
|
||||||
|
Text(it.name(), Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showDialog) {
|
||||||
|
dialog("Assignment name", assignments.map{ it.name() }, { showDialog = false }, "", onAdd)
|
||||||
|
}
|
||||||
|
else if(editing != -1) {
|
||||||
|
AddStringDialog("Assignment name", assignments.map { it.name() }, { editing = -1 }, assignments[editing].name()) {
|
||||||
|
onUpdate(assignments[editing], it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(deleting != -1) {
|
||||||
|
ConfirmDeleteDialog(
|
||||||
|
"an assignment",
|
||||||
|
{ deleting = -1 },
|
||||||
|
{ onDelete(assignments[deleting]) }
|
||||||
|
) { Text(assignments[deleting].name()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -294,42 +370,3 @@ fun StudentDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupsWidget(
|
|
||||||
course: Course, edition: Edition, groups: List<Group>, selected: Int?, onSelect: (Int) -> Unit,
|
|
||||||
deleter: (Group) -> Unit, onAdd: (name: String) -> Unit, onUpdate: (Group, String) -> Unit
|
|
||||||
) = EditionSideWidget(
|
|
||||||
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) } },
|
|
||||||
deleter
|
|
||||||
) { onExit ->
|
|
||||||
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AssignmentsWidget(
|
|
||||||
course: Course, edition: Edition, assignments: List<SoloAssignment>, selected: Int?,
|
|
||||||
onSelect: (Int) -> Unit, deleter: (SoloAssignment) -> 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)) },
|
|
||||||
{ current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } },
|
|
||||||
deleter
|
|
||||||
) { onExit ->
|
|
||||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupAssignmentsWidget(
|
|
||||||
course: Course, edition: Edition, assignments: List<GroupAssignment>, selected: Int?,
|
|
||||||
onSelect: (Int) -> Unit, deleter: (GroupAssignment) -> 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)) },
|
|
||||||
{ current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } },
|
|
||||||
deleter
|
|
||||||
) { onExit ->
|
|
||||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
|
||||||
}
|
|
|
@ -1,42 +1,46 @@
|
||||||
package com.jaytux.grader.ui
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||||
import androidx.compose.material.icons.filled.Circle
|
import androidx.compose.material.icons.filled.Circle
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.ContentPaste
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.ParagraphStyle
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
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.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
|
import com.jaytux.grader.loadClipboard
|
||||||
|
import com.jaytux.grader.toClipboard
|
||||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
|
|
||||||
@OptIn(ExperimentalRichTextApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextStyleRow(
|
fun RichTextStyleRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: RichTextState,
|
state: RichTextState,
|
||||||
) {
|
) {
|
||||||
|
val clip = LocalClipboardManager.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Row(modifier.fillMaxWidth()) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
RichTextStyleButton(
|
RichTextStyleButton(
|
||||||
|
@ -186,6 +190,14 @@ fun RichTextStyleRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconButton({ state.toClipboard(clip) }) {
|
||||||
|
Icon(Icons.Default.ContentCopy, contentDescription = "Copy markdown")
|
||||||
|
}
|
||||||
|
IconButton({ state.loadClipboard(clip, scope) }) {
|
||||||
|
Icon(Icons.Default.ContentPaste, contentDescription = "Paste markdown")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -17,12 +17,14 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogWindow
|
import androidx.compose.ui.window.DialogWindow
|
||||||
import androidx.compose.ui.window.WindowPosition
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
import androidx.compose.ui.window.rememberDialogState
|
||||||
|
import com.jaytux.grader.data.Group
|
||||||
|
import com.jaytux.grader.data.Student
|
||||||
import com.jaytux.grader.maxN
|
import com.jaytux.grader.maxN
|
||||||
import com.jaytux.grader.viewmodel.GroupState
|
import com.jaytux.grader.viewmodel.GroupState
|
||||||
import com.jaytux.grader.viewmodel.StudentState
|
import com.jaytux.grader.viewmodel.StudentState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StudentView(state: StudentState) {
|
fun StudentView(state: StudentState, nav: Navigators) {
|
||||||
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 groupGrades by state.groupGrades.entities
|
||||||
|
@ -48,9 +50,9 @@ fun StudentView(state: StudentState) {
|
||||||
Column(Modifier.weight(0.45f)) {
|
Column(Modifier.weight(0.45f)) {
|
||||||
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
||||||
Row {
|
|
||||||
val (group, c) = it
|
val (group, c) = it
|
||||||
val (course, ed) = c
|
val (course, ed) = c
|
||||||
|
Row(Modifier.clickable { nav.group(group) }) {
|
||||||
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||||
Spacer(Modifier.width(5.dp))
|
Spacer(Modifier.width(5.dp))
|
||||||
Text(
|
Text(
|
||||||
|
@ -144,7 +146,7 @@ fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupView(state: GroupState) {
|
fun GroupView(state: GroupState, nav: Navigators) {
|
||||||
val members by state.members.entities
|
val members by state.members.entities
|
||||||
val available by state.availableStudents.entities
|
val available by state.availableStudents.entities
|
||||||
val allRoles by state.roles.entities
|
val allRoles by state.roles.entities
|
||||||
|
@ -159,7 +161,7 @@ fun GroupView(state: GroupState) {
|
||||||
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
||||||
val (student, role) = it
|
val (student, role) = it
|
||||||
Row {
|
Row(Modifier.clickable { nav.student(student) }) {
|
||||||
Text(
|
Text(
|
||||||
"${student.name} (${role ?: "no role"})",
|
"${student.name} (${role ?: "no role"})",
|
||||||
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
||||||
|
@ -177,7 +179,7 @@ fun GroupView(state: GroupState) {
|
||||||
Column(Modifier.weight(0.5f)) {
|
Column(Modifier.weight(0.5f)) {
|
||||||
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
||||||
Row(Modifier.padding(5.dp)) {
|
Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
|
||||||
IconButton({ state.addStudent(it) }) {
|
IconButton({ state.addStudent(it) }) {
|
||||||
Icon(ChevronLeft, "Add student")
|
Icon(ChevronLeft, "Add student")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
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
|
||||||
|
@ -31,8 +33,6 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.format.DateTimeFormat
|
|
||||||
import kotlinx.datetime.format.byUnicodePattern
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -74,7 +74,7 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, cur
|
||||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
var name by remember(current) { mutableStateOf(current) }
|
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)
|
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) {
|
||||||
onSave(name)
|
onSave(name)
|
||||||
onClose()
|
onClose()
|
||||||
|
@ -84,6 +84,60 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, cur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmDeleteDialog(
|
||||||
|
deleteAWhat: String,
|
||||||
|
onExit: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
render: @Composable () -> Unit
|
||||||
|
) = DialogWindow(
|
||||||
|
onCloseRequest = onExit,
|
||||||
|
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp))
|
||||||
|
render()
|
||||||
|
CancelSaveRow(true, onExit, "Cancel", "Delete") {
|
||||||
|
onDelete()
|
||||||
|
onExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> ListOrEmpty(
|
||||||
|
data: List<T>,
|
||||||
|
onEmpty: @Composable ColumnScope.() -> Unit,
|
||||||
|
addOptions: @Composable ColumnScope.() -> Unit,
|
||||||
|
addAfterLazy: Boolean = true,
|
||||||
|
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||||
|
) {
|
||||||
|
if(data.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
onEmpty()
|
||||||
|
addOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Column {
|
||||||
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
itemsIndexed(data) { idx, it ->
|
||||||
|
item(idx, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!addAfterLazy) item { addOptions() }
|
||||||
|
}
|
||||||
|
if(addAfterLazy) addOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ListOrEmpty(
|
fun <T> ListOrEmpty(
|
||||||
data: List<T>,
|
data: List<T>,
|
||||||
|
@ -92,41 +146,12 @@ fun <T> ListOrEmpty(
|
||||||
onAdd: () -> Unit,
|
onAdd: () -> Unit,
|
||||||
addAfterLazy: Boolean = true,
|
addAfterLazy: Boolean = true,
|
||||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||||
) {
|
) = ListOrEmpty(
|
||||||
if(data.isEmpty()) {
|
data, emptyText,
|
||||||
Box(Modifier.fillMaxSize()) {
|
{ Button(onAdd, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) { addText() } },
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
addAfterLazy,
|
||||||
emptyText()
|
item
|
||||||
Button(onAdd, Modifier.align(Alignment.CenterHorizontally)) {
|
)
|
||||||
addText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Column {
|
|
||||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
|
||||||
itemsIndexed(data) { idx, it ->
|
|
||||||
item(idx, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!addAfterLazy) {
|
|
||||||
item {
|
|
||||||
Button(onAdd, Modifier.fillMaxWidth()) {
|
|
||||||
addText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(addAfterLazy) {
|
|
||||||
Button(onAdd, Modifier.fillMaxWidth()) {
|
|
||||||
addText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ListOrEmpty(
|
fun <T> ListOrEmpty(
|
||||||
|
@ -349,3 +374,24 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{
|
||||||
Text(italic, fontStyle = FontStyle.Italic)
|
Text(italic, fontStyle = FontStyle.Italic)
|
||||||
Text(normal)
|
Text(normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectEditDeleteRow(
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
|
||||||
|
content: @Composable BoxScope.() -> Unit
|
||||||
|
) = Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
|
||||||
|
tonalElevation = if (isSelected) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() }
|
||||||
|
IconButton(onEdit, Modifier.align(Alignment.CenterVertically)) {
|
||||||
|
Icon(Icons.Default.Edit, "Edit")
|
||||||
|
}
|
||||||
|
IconButton(onDelete, Modifier.align(Alignment.CenterVertically)) {
|
||||||
|
Icon(Icons.Default.Delete, "Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,34 @@ 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()))
|
||||||
|
|
||||||
|
enum class AssignmentType { Solo, Group }
|
||||||
|
sealed class Assignment {
|
||||||
|
class GAssignment(val assignment: GroupAssignment) : Assignment() {
|
||||||
|
override fun name(): String = assignment.name
|
||||||
|
override fun id(): EntityID<UUID> = assignment.id
|
||||||
|
}
|
||||||
|
class SAssignment(val assignment: SoloAssignment) : Assignment() {
|
||||||
|
override fun name(): String = assignment.name
|
||||||
|
override fun id(): EntityID<UUID> = assignment.id
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun name(): String
|
||||||
|
abstract fun id(): EntityID<UUID>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(assignment: GroupAssignment) = GAssignment(assignment)
|
||||||
|
fun from(assignment: SoloAssignment) = SAssignment(assignment)
|
||||||
|
|
||||||
|
fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>): List<Assignment> {
|
||||||
|
val g = groups.map { from(it) }
|
||||||
|
val s = solos.map { from(it) }
|
||||||
|
return (g + s).sortedBy {
|
||||||
|
(it as? GAssignment)?.assignment?.name ?: (it as SAssignment).assignment.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
||||||
|
|
||||||
private val rawEntities by lazy {
|
private val rawEntities by lazy {
|
||||||
|
@ -84,7 +112,12 @@ class EditionState(val edition: Edition) {
|
||||||
if(addToEdition) students.refresh()
|
if(addToEdition) students.refresh()
|
||||||
else availableStudents.refresh()
|
else availableStudents.refresh()
|
||||||
}
|
}
|
||||||
|
fun setStudentName(student: Student, name: String) {
|
||||||
|
transaction {
|
||||||
|
student.name = name
|
||||||
|
}
|
||||||
|
students.refresh()
|
||||||
|
}
|
||||||
fun addToCourse(students: List<Student>) {
|
fun addToCourse(students: List<Student>) {
|
||||||
transaction {
|
transaction {
|
||||||
EditionStudents.batchInsert(students) {
|
EditionStudents.batchInsert(students) {
|
||||||
|
@ -92,7 +125,7 @@ class EditionState(val edition: Edition) {
|
||||||
this[studentId] = it.id
|
this[studentId] = it.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
availableStudents.refresh();
|
availableStudents.refresh()
|
||||||
this.students.refresh()
|
this.students.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +172,15 @@ class EditionState(val edition: Edition) {
|
||||||
groupAs.refresh()
|
groupAs.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newAssignment(type: AssignmentType, name: String) = when(type) {
|
||||||
|
AssignmentType.Solo -> newSoloAssignment(name)
|
||||||
|
AssignmentType.Group -> newGroupAssignment(name)
|
||||||
|
}
|
||||||
|
fun setAssignmentTitle(assignment: Assignment, title: String) = when(assignment) {
|
||||||
|
is Assignment.GAssignment -> setGroupAssignmentTitle(assignment.assignment, title)
|
||||||
|
is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title)
|
||||||
|
}
|
||||||
|
|
||||||
fun delete(s: Student) {
|
fun delete(s: Student) {
|
||||||
transaction {
|
transaction {
|
||||||
EditionStudents.deleteWhere { studentId eq s.id }
|
EditionStudents.deleteWhere { studentId eq s.id }
|
||||||
|
@ -171,6 +213,10 @@ class EditionState(val edition: Edition) {
|
||||||
}
|
}
|
||||||
groupAs.refresh()
|
groupAs.refresh()
|
||||||
}
|
}
|
||||||
|
fun delete(assignment: Assignment) = when(assignment) {
|
||||||
|
is Assignment.GAssignment -> delete(assignment.assignment)
|
||||||
|
is Assignment.SAssignment -> delete(assignment.assignment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StudentState(val student: Student, edition: Edition) {
|
class StudentState(val student: Student, edition: Edition) {
|
||||||
|
@ -337,7 +383,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||||
it[this.grade] = grd
|
it[this.grade] = grd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedback.refresh()
|
feedback.refresh(); autofill.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) {
|
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) {
|
||||||
|
@ -350,6 +396,59 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||||
it[this.grade] = grd
|
it[this.grade] = grd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
feedback.refresh(); autofill.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTask(t: String) {
|
||||||
|
transaction {
|
||||||
|
assignment.assignment = t
|
||||||
|
}
|
||||||
|
_task.value = t
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDeadline(d: LocalDateTime) {
|
||||||
|
transaction {
|
||||||
|
assignment.deadline = d
|
||||||
|
}
|
||||||
|
_deadline.value = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoloAssignmentState(val assignment: SoloAssignment) {
|
||||||
|
data class LocalFeedback(val feedback: String, val grade: String)
|
||||||
|
|
||||||
|
val editionCourse = transaction { assignment.edition.course to assignment.edition }
|
||||||
|
private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
|
||||||
|
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||||
|
val feedback = RawDbState { loadFeedback() }
|
||||||
|
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||||
|
|
||||||
|
val autofill = RawDbState {
|
||||||
|
SoloFeedbacks.selectAll().where { SoloFeedbacks.soloAssignmentId eq assignment.id }.map {
|
||||||
|
it[SoloFeedbacks.feedback].split('\n')
|
||||||
|
}.flatten().distinct().sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Transaction.loadFeedback(): List<Pair<Student, LocalFeedback?>> {
|
||||||
|
val students = editionCourse.second.soloStudents
|
||||||
|
val feedbacks = SoloFeedbacks.selectAll().where {
|
||||||
|
SoloFeedbacks.soloAssignmentId eq assignment.id
|
||||||
|
}.associate {
|
||||||
|
it[SoloFeedbacks.studentId] to LocalFeedback(it[SoloFeedbacks.feedback], it[SoloFeedbacks.grade])
|
||||||
|
}
|
||||||
|
|
||||||
|
return students.map { s -> s to feedbacks[s.id] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upsertFeedback(student: Student, msg: String, grd: String) {
|
||||||
|
transaction {
|
||||||
|
SoloFeedbacks.upsert {
|
||||||
|
it[soloAssignmentId] = assignment.id
|
||||||
|
it[studentId] = student.id
|
||||||
|
it[this.feedback] = msg
|
||||||
|
it[this.grade] = grd
|
||||||
|
}
|
||||||
|
}
|
||||||
feedback.refresh()
|
feedback.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue