UI changes, slight database changes, autocompleting text field
This commit is contained in:
parent
2c4de6d8aa
commit
c8b605353c
|
@ -34,7 +34,7 @@ fun App() {
|
|||
}
|
||||
}
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.padding(10.dp)) {
|
||||
Box {
|
||||
stack.last().content { stack += (it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,12 +32,14 @@ object Students : UUIDTable("students") {
|
|||
val note = text("note")
|
||||
}
|
||||
|
||||
object GroupStudents : CompositeIdTable("grpStudents") {
|
||||
object GroupStudents : UUIDTable("grpStudents") {
|
||||
val groupId = reference("group_id", Groups.id)
|
||||
val studentId = reference("student_id", Students.id)
|
||||
val role = varchar("role", 50).nullable()
|
||||
|
||||
override val primaryKey = PrimaryKey(groupId, studentId)
|
||||
init {
|
||||
uniqueIndex(groupId, studentId) // can't figure out how to make this a composite key
|
||||
}
|
||||
}
|
||||
|
||||
object EditionStudents : Table("editionStudents") {
|
||||
|
|
|
@ -23,7 +23,7 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||
val groups by Group referrersOn Groups.editionId
|
||||
val soloStudents by Student via EditionStudents
|
||||
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
|
||||
// val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
||||
val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
||||
}
|
||||
|
||||
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
|
@ -32,6 +32,15 @@ class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||
var edition by Edition referencedOn Groups.editionId
|
||||
var name by Groups.name
|
||||
val students by Student via GroupStudents
|
||||
val studentRoles by GroupMember referrersOn GroupStudents.groupId
|
||||
}
|
||||
|
||||
class GroupMember(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, GroupMember>(GroupStudents)
|
||||
|
||||
var student by Student referencedOn GroupStudents.studentId
|
||||
var group by Group referencedOn GroupStudents.groupId
|
||||
var role by GroupStudents.role
|
||||
}
|
||||
|
||||
class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
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.OutlinedTextField
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||
|
||||
@Composable
|
||||
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||
val (course, edition) = state.editionCourse
|
||||
val name by state.name
|
||||
val task by state.task
|
||||
val allFeedback by state.feedback.entities
|
||||
|
||||
var idx by remember { mutableStateOf(0) }
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(name, "group assignment", course, edition)
|
||||
if(allFeedback.any { it.second.feedback == null }) {
|
||||
Text("Groups in bold have no feedback yet.", fontStyle = FontStyle.Italic)
|
||||
}
|
||||
else {
|
||||
Text("All groups have feedback.", fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
||||
TabRow(idx) {
|
||||
Tab(idx == 0, { idx = 0 }) { Text("Assignment") }
|
||||
allFeedback.forEachIndexed { i, it ->
|
||||
val (group, feedback) = it
|
||||
Tab(idx == i + 1, { idx = i + 1 }) {
|
||||
Text(group.name, fontWeight = feedback.feedback?.let { FontWeight.Normal } ?: FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(idx == 0) {
|
||||
var updTask by remember { mutableStateOf(task) }
|
||||
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) }
|
||||
}
|
||||
else {
|
||||
val (group, feedback, individual) = allFeedback[idx - 1].second
|
||||
groupFeedback(state, allFeedback[idx - 1].second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
||||
val (group, feedback, individual) = fdbk
|
||||
var grade by remember(fdbk) { mutableStateOf(feedback?.grade ?: "") }
|
||||
var msg by remember(fdbk) { mutableStateOf(TextFieldValue(feedback?.feedback ?: "")) }
|
||||
var idx by remember(fdbk) { mutableStateOf(0) }
|
||||
val suggestions by state.autofill.entities
|
||||
|
||||
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("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(individual.toList()) { i, (student, details) ->
|
||||
val (role, _) = details
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
||||
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||
if(idx == 0) {
|
||||
Row {
|
||||
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||
Spacer(Modifier.weight(0.6f))
|
||||
Button({ state.upsertGroupFeedback(group, msg.text, grade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = grade.isNotBlank() || msg.text.isNotBlank()) {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
|
||||
AutocompleteLineField(
|
||||
msg, { msg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
}
|
||||
}
|
||||
else {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,8 @@ package com.jaytux.grader.ui
|
|||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
|
@ -18,11 +15,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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 com.jaytux.grader.UiRoute
|
||||
import com.jaytux.grader.data.Edition
|
||||
import com.jaytux.grader.viewmodel.CourseListState
|
||||
|
@ -34,26 +27,14 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
|||
val data by state.courses.entities
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if(data.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally))
|
||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text("Add a course")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(data) { CourseWidget(state.getEditions(it), { state.delete(it) }, push) }
|
||||
|
||||
item {
|
||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
||||
Text("Add course")
|
||||
}
|
||||
}
|
||||
}
|
||||
ListOrEmpty(
|
||||
data,
|
||||
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
||||
{ Text("Add a course") },
|
||||
{ showDialog = true },
|
||||
addAfterLazy = false
|
||||
) { _, it ->
|
||||
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
||||
}
|
||||
|
||||
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||
|
|
|
@ -21,82 +21,126 @@ import androidx.compose.ui.window.WindowPosition
|
|||
import androidx.compose.ui.window.rememberDialogState
|
||||
import com.jaytux.grader.data.*
|
||||
import com.jaytux.grader.viewmodel.EditionState
|
||||
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 }
|
||||
data class Current(val p: Panel, val i: Int)
|
||||
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 }
|
||||
fun Current?.groupAsIdx() = this?.let { if(p == Panel.GroupAs) i else null }
|
||||
|
||||
@Composable
|
||||
fun EditionView(state: EditionState) = Row {
|
||||
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||
var isGroup by remember { mutableStateOf(false) }
|
||||
var idx by remember { mutableStateOf<Current?>(null) }
|
||||
|
||||
val students by state.students.entities
|
||||
val groups by state.groups.entities
|
||||
val solo by state.solo.entities
|
||||
val groupAs by state.groupAs//.entities
|
||||
val groupAs by state.groupAs.entities
|
||||
|
||||
val toggle = { i: Int, p: Panel ->
|
||||
idx = if(idx?.p == p && idx?.i == i) null else Current(p, i)
|
||||
}
|
||||
|
||||
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
||||
TabLayout(
|
||||
listOf("Students", "Groups"),
|
||||
if(isGroup) 1 else 0,
|
||||
if (isGroup) 1 else 0,
|
||||
{ isGroup = it == 1 },
|
||||
{ Text(it) },
|
||||
Modifier.weight(0.25f)
|
||||
{ Text(it) }
|
||||
) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
if(isGroup) {
|
||||
if (isGroup) {
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
GroupsWidget(state.course, state.edition, groups, {}) { state.newGroup(it) }
|
||||
GroupsWidget(
|
||||
state.course,
|
||||
state.edition,
|
||||
groups,
|
||||
idx.groupIdx(),
|
||||
{ toggle(it, Panel.Group) }) {
|
||||
state.newGroup(it)
|
||||
}
|
||||
Box(Modifier.weight(0.5f)) { GroupAssignmentsWidget(groupAs, {}) {} }
|
||||
}
|
||||
else {
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
StudentsWidget(state.course, state.edition, students, {}) { name, note, contact, addToEdition ->
|
||||
state.newStudent(name, note, contact, addToEdition)
|
||||
GroupAssignmentsWidget(
|
||||
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) }
|
||||
) {
|
||||
state.newGroupAssignment(it)
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.5f)) { AssignmentsWidget(solo, {}) {} }
|
||||
} else {
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
StudentsWidget(
|
||||
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) }
|
||||
) { 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.newSoloAssignment(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.75f)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.75f)) {
|
||||
idx?.let { i ->
|
||||
when(i.p) {
|
||||
Panel.Student -> StudentView(StudentState(students[i.i], state.edition))
|
||||
Panel.Group -> GroupView(GroupState(groups[i.i]))
|
||||
Panel.GroupAs -> GroupAssignmentView(GroupAssignmentState(groupAs[i.i]))
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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,
|
||||
dialog: @Composable (onExit: () -> Unit) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
Text(header, style = MaterialTheme.typography.headlineMedium)
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
ListOrEmpty(
|
||||
data,
|
||||
{ Text("Course ${course.name} (edition ${edition.name})\nhas no $hasNoX yet.", Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center) },
|
||||
{ Text("Add $addX") },
|
||||
{ showDialog = true }
|
||||
) { idx, it ->
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { onSelect(idx) },
|
||||
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
singleWidget(it)
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) dialog { showDialog = false }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StudentsWidget(
|
||||
course: Course,
|
||||
edition: Edition,
|
||||
students: List<Student>,
|
||||
onSelect: (Int) -> Unit,
|
||||
course: Course, edition: Edition, students: List<Student>, selected: Int?, onSelect: (Int) -> Unit,
|
||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
Text("Student list", style = MaterialTheme.typography.headlineMedium)
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
if(students.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
Text(
|
||||
"Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text("Add a student")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||
itemsIndexed(students) { idx, it ->
|
||||
Surface(Modifier.fillMaxWidth().clickable { onSelect(idx) }) {
|
||||
Text(it.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
||||
Text("Add a student")
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) StudentDialog(course, edition, { showDialog = false }, onAdd)
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Student list", "students", "a student", students, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||
) { onExit ->
|
||||
StudentDialog(course, edition, onExit, onAdd)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -135,52 +179,33 @@ fun StudentDialog(
|
|||
|
||||
@Composable
|
||||
fun GroupsWidget(
|
||||
course: Course,
|
||||
edition: Edition,
|
||||
groups: List<Group>,
|
||||
onSelect: (Int) -> Unit,
|
||||
course: Course, edition: Edition, groups: List<Group>, selected: Int?, onSelect: (Int) -> Unit,
|
||||
onAdd: (name: String) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
Text("Group list", style = MaterialTheme.typography.headlineMedium)
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if(groups.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
Text(
|
||||
"Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text("Add a group")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||
itemsIndexed(groups) { idx, it ->
|
||||
Surface(Modifier.fillMaxWidth().clickable { onSelect(idx) }) {
|
||||
Text(it.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
||||
Text("Add a group")
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) AddStringDialog("Group name", groups.map { it.name }, { showDialog = false }) { onAdd(it) }
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Group list", "groups", "a group", groups, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||
) { onExit ->
|
||||
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AssignmentsWidget(assignments: List<SoloAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
||||
//
|
||||
fun AssignmentsWidget(
|
||||
course: Course, edition: Edition, assignments: List<SoloAssignment>, selected: Int?,
|
||||
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Assignment list", "assignments", "an assignment", assignments, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||
) { onExit ->
|
||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupAssignmentsWidget(assignments: List<GroupAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
||||
//
|
||||
fun GroupAssignmentsWidget(
|
||||
course: Course, edition: Edition, assignments: List<GroupAssignment>, selected: Int?,
|
||||
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Group assignment list", "group assignments", "an assignment", assignments, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||
) { onExit ->
|
||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 com.jaytux.grader.viewmodel.GroupState
|
||||
import com.jaytux.grader.viewmodel.StudentState
|
||||
|
||||
@Composable
|
||||
fun StudentView(state: StudentState) {
|
||||
val groups by state.groups.entities
|
||||
val courses by state.courseEditions.entities
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(state.student.name, "student", state.editionCourse)
|
||||
Row {
|
||||
Column(Modifier.padding(10.dp).weight(0.45f)) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
|
||||
InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
|
||||
InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
|
||||
}
|
||||
Box(Modifier.weight(0.55f)) {}
|
||||
}
|
||||
Row {
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
|
||||
val (ed, course) = it
|
||||
Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
||||
Row {
|
||||
val (group, c) = it
|
||||
val (course, ed) = c
|
||||
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
Text("(in course $course ($ed))", Modifier.align(Alignment.Bottom), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupView(state: GroupState) {
|
||||
val members by state.members.entities
|
||||
val available by state.availableStudents.entities
|
||||
val allRoles by state.roles.entities
|
||||
val (course, edition) = state.course
|
||||
|
||||
var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(state.group.name, "group", course, edition)
|
||||
Row {
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
||||
val (student, role) = it
|
||||
Row {
|
||||
Text(
|
||||
"${student.name} (${role ?: "no role"})",
|
||||
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
|
||||
Icon(Icons.Default.Edit, "Change role")
|
||||
}
|
||||
IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
|
||||
Icon(Icons.Default.Delete, "Remove student")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
||||
Row(Modifier.padding(5.dp)) {
|
||||
IconButton({ state.addStudent(it) }) {
|
||||
Icon(ChevronLeft, "Add student")
|
||||
}
|
||||
Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pickRole?.let {
|
||||
val (curr, onPick) = it
|
||||
RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
|
||||
onCloseRequest = onClose,
|
||||
state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
|
||||
) {
|
||||
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
var role by remember { mutableStateOf(curr ?: "") }
|
||||
Column {
|
||||
Text("Used roles:")
|
||||
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
|
||||
items(used) {
|
||||
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
|
||||
Text(it, Modifier.padding(5.dp))
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
}
|
||||
}
|
||||
OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
|
||||
CancelSaveRow(true, onClose) {
|
||||
onSave(role.ifBlank { null })
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +1,43 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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 com.jaytux.grader.data.Course
|
||||
import com.jaytux.grader.data.Edition
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, onSave: () -> Unit) {
|
||||
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
||||
Row {
|
||||
Button({ onCancel() }, Modifier.weight(0.45f)) { Text("Cancel") }
|
||||
Button({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) }
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text("Save") }
|
||||
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,3 +80,178 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, onS
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ListOrEmpty(
|
||||
data: List<T>,
|
||||
emptyText: @Composable ColumnScope.() -> Unit,
|
||||
addText: @Composable RowScope.() -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
addAfterLazy: Boolean = true,
|
||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||
) {
|
||||
if(data.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
emptyText()
|
||||
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
|
||||
fun <T> ListOrEmpty(
|
||||
data: List<T>,
|
||||
emptyText: @Composable ColumnScope.() -> Unit,
|
||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||
) {
|
||||
if(data.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
emptyText()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Column {
|
||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||
itemsIndexed(data) { idx, it ->
|
||||
item(idx, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InteractToEdit(
|
||||
content: String, onSave: (String) -> Unit, pre: String, modifier: Modifier = Modifier,
|
||||
w1: Float = 0.75f, w2: Float = 0.25f,
|
||||
singleLine: Boolean = true
|
||||
) {
|
||||
var text by remember(content) { mutableStateOf(content) }
|
||||
|
||||
Row(modifier.padding(5.dp)) {
|
||||
val base = if(singleLine) Modifier.align(Alignment.CenterVertically) else Modifier
|
||||
OutlinedTextField(
|
||||
text, { text = it }, base.weight(w1), label = { Text(pre) },
|
||||
singleLine = singleLine, minLines = if(singleLine) 1 else 5
|
||||
)
|
||||
IconButton({ onSave(text) }, base.weight(w2)) { Icon(Icons.Default.Check, "Save") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaneHeader(name: String, type: String, course: Course, edition: Edition) = Column {
|
||||
Text(name, style = MaterialTheme.typography.headlineMedium)
|
||||
Text("${type.capitalize(Locale.current)} in ${course.name} (${edition.name})", fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>) = PaneHeader(name, type, courseEdition.first, courseEdition.second)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AutocompleteLineField(
|
||||
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
||||
onFilter: (String) -> List<String>
|
||||
) = Column(modifier) {
|
||||
var suggestions by remember { mutableStateOf(listOf<String>()) }
|
||||
var selected by remember { mutableStateOf(0) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberLazyListState()
|
||||
|
||||
val posToLine = { pos: Int ->
|
||||
(value.text.take(pos).count { it == '\n' }) to (value.text.take(pos).lastIndexOf('\n'))
|
||||
}
|
||||
|
||||
val autoComplete = { str: String ->
|
||||
val pos = value.selection.start
|
||||
val lines = value.text.split("\n").toMutableList()
|
||||
val (lineno, lineStart) = posToLine(pos)
|
||||
|
||||
lines[lineno] = str
|
||||
onValueChange(value.copy(text = lines.joinToString("\n"), selection = TextRange(lineStart + str.length)))
|
||||
}
|
||||
|
||||
val currentLine = {
|
||||
value.text.split('\n')[posToLine(value.selection.start).first]
|
||||
}
|
||||
|
||||
val gotoOption = { idx: Int ->
|
||||
selected = if(suggestions.isEmpty()) 0 else ((idx + suggestions.size) % suggestions.size)
|
||||
scope.launch {
|
||||
scrollState.animateScrollToItem(if(suggestions.isNotEmpty()) (selected + 1) else 0)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
val onKey = { kev: KeyEvent ->
|
||||
var res = true
|
||||
if(suggestions.isNotEmpty()) {
|
||||
when (kev.key) {
|
||||
Key.Tab -> autoComplete(suggestions[selected])
|
||||
Key.DirectionUp -> gotoOption(selected - 1)
|
||||
Key.DirectionDown -> gotoOption(selected + 1)
|
||||
Key.Escape -> suggestions = listOf()
|
||||
else -> res = false
|
||||
}
|
||||
}
|
||||
else res = false
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
LaunchedEffect(value.text) {
|
||||
delay(300)
|
||||
suggestions = onFilter(currentLine())
|
||||
gotoOption(if(suggestions.isEmpty()) 0 else selected % suggestions.size)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value, onValueChange,
|
||||
Modifier.fillMaxWidth().weight(0.75f).onKeyEvent(onKey), label = label, singleLine = false, minLines = 5
|
||||
)
|
||||
|
||||
if(suggestions.isNotEmpty()) {
|
||||
LazyColumn(Modifier.weight(0.25f), state = scrollState) {
|
||||
stickyHeader {
|
||||
Surface(tonalElevation = 5.dp) {
|
||||
Text("Suggestions", Modifier.padding(5.dp).fillMaxWidth(), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
itemsIndexed(suggestions) { idx, it ->
|
||||
Surface(Modifier.padding(5.dp).fillMaxWidth(), tonalElevation = if(selected == idx) 50.dp else 0.dp) {
|
||||
Text(it, Modifier.clickable { autoComplete(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,14 +4,13 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.jaytux.grader.data.*
|
||||
import org.jetbrains.exposed.dao.Entity
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.util.*
|
||||
|
||||
class RawDbState<T: Entity<UUID>>(private val loader: (Transaction.() -> List<T>)) {
|
||||
private fun <T> MutableState<T>.immutable(): State<T> = this@immutable
|
||||
fun <T> MutableState<T>.immutable(): State<T> = this
|
||||
|
||||
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
||||
|
||||
private val rawEntities by lazy {
|
||||
mutableStateOf(transaction { loader() })
|
||||
|
@ -57,8 +56,8 @@ class EditionState(val edition: Edition) {
|
|||
val course = transaction { edition.course }
|
||||
val students = RawDbState { edition.soloStudents.toList() }
|
||||
val groups = RawDbState { edition.groups.toList() }
|
||||
val groupAs = mutableStateOf(listOf<GroupAssignment>())
|
||||
val solo = RawDbState { edition.soloAssignments.toList() }
|
||||
val groupAs = RawDbState { edition.groupAssignments.toList() }
|
||||
|
||||
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
|
||||
transaction {
|
||||
|
@ -78,4 +77,181 @@ class EditionState(val edition: Edition) {
|
|||
groups.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun newSoloAssignment(name: String) {
|
||||
transaction {
|
||||
SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" }
|
||||
solo.refresh()
|
||||
}
|
||||
}
|
||||
fun newGroupAssignment(name: String) {
|
||||
transaction {
|
||||
GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" }
|
||||
groupAs.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StudentState(val student: Student, edition: Edition) {
|
||||
val editionCourse = transaction { edition.course to edition }
|
||||
val groups = RawDbState { student.groups.map { it to (it.edition.course.name to it.edition.name) }.toList() }
|
||||
val courseEditions = RawDbState { student.courses.map{ it to it.course }.toList() }
|
||||
|
||||
fun update(f: Student.() -> Unit) {
|
||||
transaction {
|
||||
student.f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupState(val group: Group) {
|
||||
val members = RawDbState { group.studentRoles.map{ it.student to it.role }.toList() }
|
||||
val availableStudents = RawDbState { Student.find {
|
||||
// not yet in the group
|
||||
(Students.id notInList group.students.map { it.id }) and
|
||||
// but in the same course (edition)
|
||||
(Students.id inList group.edition.soloStudents.map { it.id })
|
||||
}.toList() }
|
||||
val course = transaction { group.edition.course to group.edition }
|
||||
val roles = RawDbState {
|
||||
GroupStudents.select(GroupStudents.role).where{ GroupStudents.role.isNotNull() }
|
||||
.withDistinct(true).map{ it[GroupStudents.role] ?: "" }.toList()
|
||||
}
|
||||
|
||||
fun addStudent(student: Student) {
|
||||
transaction {
|
||||
GroupStudents.insert {
|
||||
it[groupId] = group.id
|
||||
it[studentId] = student.id
|
||||
}
|
||||
}
|
||||
members.refresh(); availableStudents.refresh()
|
||||
}
|
||||
|
||||
fun removeStudent(student: Student) {
|
||||
transaction {
|
||||
GroupStudents.deleteWhere { groupId eq group.id and (studentId eq student.id) }
|
||||
}
|
||||
members.refresh(); availableStudents.refresh()
|
||||
}
|
||||
|
||||
fun updateRole(student: Student, role: String?) {
|
||||
transaction {
|
||||
GroupStudents.update({ GroupStudents.groupId eq group.id and (GroupStudents.studentId eq student.id) }) {
|
||||
it[this.role] = role
|
||||
}
|
||||
members.refresh()
|
||||
roles.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
data class LocalFeedback(val feedback: String, val grade: String)
|
||||
data class LocalGFeedback(
|
||||
val group: Group,
|
||||
val feedback: LocalFeedback?,
|
||||
val individuals: Map<Student, Pair<String?, LocalFeedback?>>
|
||||
)
|
||||
|
||||
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() }
|
||||
|
||||
val autofill = RawDbState {
|
||||
val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||
it[GroupFeedbacks.feedback].split('\n')
|
||||
}.toList()
|
||||
|
||||
val forIndividuals = IndividualFeedbacks.selectAll().where { IndividualFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||
it[IndividualFeedbacks.feedback].split('\n')
|
||||
}.toList()
|
||||
|
||||
(forGroups + forIndividuals).distinct()
|
||||
}
|
||||
|
||||
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
||||
val individuals = IndividualFeedbacks.selectAll().where {
|
||||
IndividualFeedbacks.groupAssignmentId eq assignment.id
|
||||
}.map {
|
||||
it[IndividualFeedbacks.studentId] to LocalFeedback(it[IndividualFeedbacks.feedback], it[IndividualFeedbacks.grade])
|
||||
}.associate { it }
|
||||
|
||||
val groupFeedbacks = GroupFeedbacks.selectAll().where {
|
||||
GroupFeedbacks.groupAssignmentId eq assignment.id
|
||||
}.map {
|
||||
it[GroupFeedbacks.groupId] to (it[GroupFeedbacks.feedback] to it[GroupFeedbacks.grade])
|
||||
}.associate { it }
|
||||
|
||||
val groups = Group.find {
|
||||
(Groups.editionId eq assignment.edition.id)
|
||||
}.map { group ->
|
||||
val students = group.studentRoles.associate { sR ->
|
||||
val student = sR.student
|
||||
val role = sR.role
|
||||
val feedback = individuals[student.id]
|
||||
|
||||
student to (role to feedback)
|
||||
}
|
||||
|
||||
groupFeedbacks[group.id]?.let { (f, g) ->
|
||||
group to LocalGFeedback(group, LocalFeedback(f, g), students)
|
||||
} ?: (group to LocalGFeedback(group, null, students))
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
fun upsertGroupFeedback(group: Group, msg: String, grd: String) {
|
||||
transaction {
|
||||
GroupFeedbacks.upsert {
|
||||
it[groupAssignmentId] = assignment.id
|
||||
it[groupId] = group.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
}
|
||||
}
|
||||
feedback.refresh()
|
||||
}
|
||||
|
||||
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) {
|
||||
transaction {
|
||||
IndividualFeedbacks.upsert {
|
||||
it[groupAssignmentId] = assignment.id
|
||||
it[groupId] = group.id
|
||||
it[studentId] = student.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
}
|
||||
}
|
||||
feedback.refresh()
|
||||
}
|
||||
|
||||
fun updateTask(t: String) {
|
||||
transaction {
|
||||
assignment.assignment = t
|
||||
}
|
||||
_task.value = t
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ kotlin = "2.1.0"
|
|||
kotlinx-coroutines = "1.10.1"
|
||||
exposed = "0.59.0"
|
||||
material3 = "1.7.3"
|
||||
ui-android = "1.7.8"
|
||||
foundation-layout-android = "1.7.8"
|
||||
|
||||
[libraries]
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
|
@ -22,6 +24,8 @@ 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" }
|
||||
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" }
|
||||
|
||||
[plugins]
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
|
|
Loading…
Reference in New Issue