Lots of layout
This commit is contained in:
parent
bccff22866
commit
2c4de6d8aa
|
@ -1,23 +1,43 @@
|
||||||
package com.jaytux.grader
|
package com.jaytux.grader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jaytux.grader.ui.ChevronLeft
|
||||||
import com.jaytux.grader.ui.CoursesView
|
import com.jaytux.grader.ui.CoursesView
|
||||||
|
import com.jaytux.grader.ui.toDp
|
||||||
import com.jaytux.grader.viewmodel.CourseListState
|
import com.jaytux.grader.viewmodel.CourseListState
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
data class UiRoute(val heading: String, val content: @Composable (push: (UiRoute) -> Unit) -> Unit)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun App() {
|
fun App() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val courseList = CourseListState()
|
val courseList = CourseListState()
|
||||||
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
var stack by remember {
|
||||||
CoursesView(courseList)
|
val start = UiRoute("Courses Overview") { CoursesView(courseList, it) }
|
||||||
|
mutableStateOf(listOf(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Surface(Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary, tonalElevation = 10.dp, shadowElevation = 10.dp) {
|
||||||
|
Row(Modifier.padding(10.dp)) {
|
||||||
|
IconButton({ stack = stack.toMutableList().also { it.removeLast() } }, enabled = stack.size >= 2) {
|
||||||
|
Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineLarge.fontSize.toDp()))
|
||||||
|
}
|
||||||
|
Text(stack.last().heading, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.padding(10.dp)) {
|
||||||
|
stack.last().content { stack += (it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,17 +28,19 @@ object Groups : UUIDTable("groups") {
|
||||||
|
|
||||||
object Students : UUIDTable("students") {
|
object Students : UUIDTable("students") {
|
||||||
val name = varchar("name", 50)
|
val name = varchar("name", 50)
|
||||||
|
val contact = varchar("contact", 50)
|
||||||
val note = text("note")
|
val note = text("note")
|
||||||
}
|
}
|
||||||
|
|
||||||
object GroupStudents : Table("grpStudents") {
|
object GroupStudents : CompositeIdTable("grpStudents") {
|
||||||
val groupId = reference("group_id", Groups.id)
|
val groupId = reference("group_id", Groups.id)
|
||||||
val studentId = reference("student_id", Students.id)
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val role = varchar("role", 50).nullable()
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(groupId, studentId)
|
override val primaryKey = PrimaryKey(groupId, studentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
object SoloStudents : Table("soloStudents") {
|
object EditionStudents : Table("editionStudents") {
|
||||||
val editionId = reference("edition_id", Editions.id)
|
val editionId = reference("edition_id", Editions.id)
|
||||||
val studentId = reference("student_id", Students.id)
|
val studentId = reference("student_id", Students.id)
|
||||||
|
|
||||||
|
@ -46,15 +48,15 @@ object SoloStudents : Table("soloStudents") {
|
||||||
}
|
}
|
||||||
|
|
||||||
object GroupAssignments : UUIDTable("grpAssgmts") {
|
object GroupAssignments : UUIDTable("grpAssgmts") {
|
||||||
val editionId = reference("edition_id_", Editions.id)
|
val editionId = reference("edition_id", Editions.id)
|
||||||
val name = varchar("name_", 50)
|
val name = varchar("name", 50)
|
||||||
val assignment = text("assignment_")
|
val assignment = text("assignment")
|
||||||
}
|
}
|
||||||
|
|
||||||
object SoloAssignments : UUIDTable("soloAssgmts") {
|
object SoloAssignments : UUIDTable("soloAssgmts") {
|
||||||
val editionId = GroupAssignments.reference("edition_id", Editions.id)
|
val editionId = reference("edition_id", Editions.id)
|
||||||
val name = GroupAssignments.varchar("name", 50)
|
val name = varchar("name", 50)
|
||||||
val assignment = GroupAssignments.text("assignment")
|
val assignment = text("assignment")
|
||||||
}
|
}
|
||||||
|
|
||||||
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||||
|
|
|
@ -10,7 +10,7 @@ object Database {
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(
|
SchemaUtils.create(
|
||||||
Courses, Editions, Groups,
|
Courses, Editions, Groups,
|
||||||
Students, GroupStudents, SoloStudents,
|
Students, GroupStudents, EditionStudents,
|
||||||
GroupAssignments, SoloAssignments,
|
GroupAssignments, SoloAssignments,
|
||||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
|
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,9 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
var course by Course referencedOn Editions.courseId
|
var course by Course referencedOn Editions.courseId
|
||||||
var name by Editions.name
|
var name by Editions.name
|
||||||
val groups by Group referrersOn Groups.editionId
|
val groups by Group referrersOn Groups.editionId
|
||||||
val soloStudents by Student via SoloStudents
|
val soloStudents by Student via EditionStudents
|
||||||
|
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
|
||||||
|
// val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
||||||
}
|
}
|
||||||
|
|
||||||
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
@ -37,8 +39,9 @@ class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
|
||||||
var name by Students.name
|
var name by Students.name
|
||||||
var note by Students.note
|
var note by Students.note
|
||||||
|
var contact by Students.contact
|
||||||
val groups by Group via GroupStudents
|
val groups by Group via GroupStudents
|
||||||
val soloCourses by Edition via SoloStudents
|
val courses by Edition via EditionStudents
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.jaytux.grader.ui
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
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.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
@ -9,7 +10,6 @@ import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
@ -20,12 +20,17 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
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.*
|
import androidx.compose.ui.window.DialogWindow
|
||||||
import com.jaytux.grader.data.Course
|
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
|
import com.jaytux.grader.viewmodel.CourseListState
|
||||||
|
import com.jaytux.grader.viewmodel.EditionListState
|
||||||
|
import com.jaytux.grader.viewmodel.EditionState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CoursesView(state: CourseListState) {
|
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) }
|
||||||
|
|
||||||
|
@ -41,7 +46,7 @@ fun CoursesView(state: CourseListState) {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
items(data) { CourseWidget(it) { state.delete(it) } }
|
items(data) { CourseWidget(state.getEditions(it), { state.delete(it) }, push) }
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
||||||
|
@ -51,45 +56,71 @@ fun CoursesView(state: CourseListState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showDialog) AddCourseDialog(data.map { it.name }, { showDialog = false }) { state.new(it) }
|
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddCourseDialog(taken: List<String>, onClose: () -> Unit, onSave: (String) -> Unit) = DialogWindow(
|
fun CourseWidget(state: EditionListState, onDelete: () -> Unit, push: (UiRoute) -> Unit) {
|
||||||
onCloseRequest = onClose,
|
val editions by state.editions.entities
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
var isOpened by remember { mutableStateOf(false) }
|
||||||
) {
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
|
||||||
var name by remember { mutableStateOf("") }
|
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
|
||||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Course name") }, isError = name in taken)
|
|
||||||
Row {
|
|
||||||
Button({ onClose() }, Modifier.weight(0.45f)) { Text("Cancel") }
|
|
||||||
Spacer(Modifier.weight(0.1f))
|
|
||||||
Button({ onSave(name); onClose() }, Modifier.weight(0.45f)) { Text("Save") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
val callback = { it: Edition ->
|
||||||
fun CourseWidget(course: Course, onDelete: () -> Unit) {
|
val s = EditionState(it)
|
||||||
val editions = remember(course) { course.loadEditions().size }
|
val route = UiRoute("${state.course.name}: ${it.name}") {
|
||||||
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp), shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
EditionView(s)
|
||||||
|
}
|
||||||
|
push(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { isOpened = !isOpened }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
||||||
Row {
|
Row {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f).padding(5.dp)) {
|
||||||
Text(course.name, Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
|
|
||||||
Row {
|
Row {
|
||||||
Spacer(Modifier.width(15.dp))
|
Icon(
|
||||||
Text("$editions editions", fontStyle = FontStyle.Italic)
|
if (isOpened) ChevronDown else ChevronRight, "Toggle editions",
|
||||||
|
Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp())
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(state.course.name, style = MaterialTheme.typography.headlineMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.width(25.dp))
|
||||||
|
Text(
|
||||||
|
"${editions.size} edition(s)",
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isOpened) {
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.width(25.dp))
|
||||||
|
Column {
|
||||||
|
editions.forEach { EditionWidget(it, { callback(it) }) { state.delete(it) } }
|
||||||
|
Button({ showDialog = true }, Modifier.fillMaxWidth()) { Text("Add edition") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
||||||
IconButton({ TODO() }) { Icon(Icons.Default.Edit, "Edit") }
|
IconButton({ TODO() }, enabled = false) { Icon(Icons.Default.Edit, "Edit") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(showDialog) AddStringDialog("Edition name", editions.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditionWidget(edition: Edition, onOpen: () -> Unit, onDelete: () -> Unit) {
|
||||||
|
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { onOpen() }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
||||||
|
Row(Modifier.padding(5.dp)) {
|
||||||
|
Text(edition.name, Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
|
||||||
|
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
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.Checkbox
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
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.*
|
||||||
|
import com.jaytux.grader.viewmodel.EditionState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditionView(state: EditionState) = Row {
|
||||||
|
var isGroup by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val students by state.students.entities
|
||||||
|
val groups by state.groups.entities
|
||||||
|
val solo by state.solo.entities
|
||||||
|
val groupAs by state.groupAs//.entities
|
||||||
|
|
||||||
|
TabLayout(
|
||||||
|
listOf("Students", "Groups"),
|
||||||
|
if(isGroup) 1 else 0,
|
||||||
|
{ isGroup = it == 1 },
|
||||||
|
{ Text(it) },
|
||||||
|
Modifier.weight(0.25f)
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
if(isGroup) {
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
GroupsWidget(state.course, state.edition, groups, {}) { 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.5f)) { AssignmentsWidget(solo, {}) {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.75f)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudentsWidget(
|
||||||
|
course: Course,
|
||||||
|
edition: Edition,
|
||||||
|
students: List<Student>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudentDialog(
|
||||||
|
course: Course,
|
||||||
|
edition: Edition,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||||
|
) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(600.dp, 400.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var contact by remember { mutableStateOf("") }
|
||||||
|
var note by remember { mutableStateOf("") }
|
||||||
|
var add by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), singleLine = true, label = { Text("Student name") })
|
||||||
|
OutlinedTextField(contact, { contact = it }, Modifier.fillMaxWidth(), singleLine = true, label = { Text("Student contact") })
|
||||||
|
OutlinedTextField(note, { note = it }, Modifier.fillMaxWidth(), singleLine = false, minLines = 3, label = { Text("Note") })
|
||||||
|
Row {
|
||||||
|
Checkbox(add, { add = it })
|
||||||
|
Text("Add student to ${course.name} ${edition.name}?", Modifier.align(Alignment.CenterVertically))
|
||||||
|
}
|
||||||
|
CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
|
||||||
|
onAdd(name, note, contact, add)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupsWidget(
|
||||||
|
course: Course,
|
||||||
|
edition: Edition,
|
||||||
|
groups: List<Group>,
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AssignmentsWidget(assignments: List<SoloAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupAssignmentsWidget(assignments: List<GroupAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
||||||
|
//
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val ChevronRight: ImageVector by lazy {
|
||||||
|
ImageVector.Builder(
|
||||||
|
name = "ChevronRight",
|
||||||
|
defaultWidth = 24.dp,
|
||||||
|
defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24f,
|
||||||
|
viewportHeight = 24f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = null,
|
||||||
|
fillAlpha = 1.0f,
|
||||||
|
stroke = SolidColor(Color(0xFF000000)),
|
||||||
|
strokeAlpha = 1.0f,
|
||||||
|
strokeLineWidth = 2f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
strokeLineJoin = StrokeJoin.Round,
|
||||||
|
strokeLineMiter = 1.0f,
|
||||||
|
pathFillType = PathFillType.NonZero
|
||||||
|
) {
|
||||||
|
moveTo(9f, 18f)
|
||||||
|
lineToRelative(6f, -6f)
|
||||||
|
lineToRelative(-6f, -6f)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ChevronDown: ImageVector by lazy {
|
||||||
|
ImageVector.Builder(
|
||||||
|
name = "ChevronDown",
|
||||||
|
defaultWidth = 24.dp,
|
||||||
|
defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24f,
|
||||||
|
viewportHeight = 24f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = null,
|
||||||
|
fillAlpha = 1.0f,
|
||||||
|
stroke = SolidColor(Color(0xFF000000)),
|
||||||
|
strokeAlpha = 1.0f,
|
||||||
|
strokeLineWidth = 2f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
strokeLineJoin = StrokeJoin.Round,
|
||||||
|
strokeLineMiter = 1.0f,
|
||||||
|
pathFillType = PathFillType.NonZero
|
||||||
|
) {
|
||||||
|
moveTo(6f, 9f)
|
||||||
|
lineToRelative(6f, 6f)
|
||||||
|
lineToRelative(6f, -6f)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
public val ChevronLeft: ImageVector by lazy {
|
||||||
|
ImageVector.Builder(
|
||||||
|
name = "ChevronLeft",
|
||||||
|
defaultWidth = 24.dp,
|
||||||
|
defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24f,
|
||||||
|
viewportHeight = 24f
|
||||||
|
).apply {
|
||||||
|
path(
|
||||||
|
fill = null,
|
||||||
|
fillAlpha = 1.0f,
|
||||||
|
stroke = SolidColor(Color(0xFF000000)),
|
||||||
|
strokeAlpha = 1.0f,
|
||||||
|
strokeLineWidth = 2f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
strokeLineJoin = StrokeJoin.Round,
|
||||||
|
strokeLineMiter = 1.0f,
|
||||||
|
pathFillType = PathFillType.NonZero
|
||||||
|
) {
|
||||||
|
moveTo(15f, 18f)
|
||||||
|
lineToRelative(-6f, -6f)
|
||||||
|
lineToRelative(6f, -6f)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextUnit.toDp(): Dp = with(LocalDensity.current) { value.toDp() }
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, onSave: () -> Unit) {
|
||||||
|
Row {
|
||||||
|
Button({ onCancel() }, Modifier.weight(0.45f)) { Text("Cancel") }
|
||||||
|
Spacer(Modifier.weight(0.1f))
|
||||||
|
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text("Save") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> TabLayout(
|
||||||
|
options: List<T>,
|
||||||
|
currentIndex: Int,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
optionContent: @Composable (T) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) = Column(modifier) {
|
||||||
|
TabRow(currentIndex) {
|
||||||
|
options.forEachIndexed { idx, it ->
|
||||||
|
Tab(
|
||||||
|
selected = idx == currentIndex,
|
||||||
|
onClick = { onSelect(idx) },
|
||||||
|
text = { optionContent(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, onSave: (String) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
||||||
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
|
onSave(name)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,10 @@ package com.jaytux.grader.viewmodel
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import com.jaytux.grader.data.Course
|
import com.jaytux.grader.data.*
|
||||||
import com.jaytux.grader.data.Edition
|
|
||||||
import com.jaytux.grader.data.Editions
|
|
||||||
import org.jetbrains.exposed.dao.Entity
|
import org.jetbrains.exposed.dao.Entity
|
||||||
import org.jetbrains.exposed.sql.Transaction
|
import org.jetbrains.exposed.sql.Transaction
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -36,8 +35,47 @@ class CourseListState {
|
||||||
transaction { course.delete() }
|
transaction { course.delete() }
|
||||||
courses.refresh()
|
courses.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEditions(course: Course) = EditionListState(course)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditionListState(private val course: Course) {
|
class EditionListState(val course: Course) {
|
||||||
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.toList() }
|
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.toList() }
|
||||||
|
|
||||||
|
fun new(name: String) {
|
||||||
|
transaction { Edition.new { this.name = name; this.course = this@EditionListState.course } }
|
||||||
|
editions.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(edition: Edition) {
|
||||||
|
transaction { edition.delete() }
|
||||||
|
editions.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() }
|
||||||
|
|
||||||
|
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
|
||||||
|
transaction {
|
||||||
|
val student = Student.new { this.name = name; this.contact = contact; this.note = note }
|
||||||
|
if(addToEdition) EditionStudents.insert {
|
||||||
|
it[editionId] = edition.id
|
||||||
|
it[studentId] = student.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addToEdition) students.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newGroup(name: String) {
|
||||||
|
transaction {
|
||||||
|
Group.new { this.name = name; this.edition = this@EditionState.edition }
|
||||||
|
groups.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue