Jewel-ize part II

This commit is contained in:
2026-03-29 16:06:33 +02:00
parent 18a7a82c36
commit 28c3b29c3a
15 changed files with 351 additions and 225 deletions

View File

@@ -48,6 +48,7 @@ kotlin {
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.compose.backhandler) implementation(libs.compose.backhandler)
implementation(libs.jewel) implementation(libs.jewel)
implementation(libs.jewel.windows)
} }
} }
} }

View File

@@ -14,6 +14,7 @@ import com.jaytux.grader.ui.PeerEvalsGradingTitle
import com.jaytux.grader.ui.PeerEvalsGradingView import com.jaytux.grader.ui.PeerEvalsGradingView
import com.jaytux.grader.ui.SolosGradingTitle import com.jaytux.grader.ui.SolosGradingTitle
import com.jaytux.grader.ui.SolosGradingView import com.jaytux.grader.ui.SolosGradingView
import com.jaytux.grader.ui.Surface
import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
@@ -26,12 +27,14 @@ data class PeerEvalGrading(val course: Course, val edition: Edition, val assignm
@Composable @Composable
fun App() { fun App() {
IntUiTheme(isDark = true) { IntUiTheme(isDark = true) {
Navigator.NavHost(Home) { Surface {
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) } Navigator.NavHost(Home) {
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) } composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
composable<GroupGrading>({ GroupsGradingTitle(it) }) { data, token -> GroupsGradingView(data, token) } composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }
composable<SoloGrading>({ SolosGradingTitle(it) }) { data, token -> SolosGradingView(data, token) } composable<GroupGrading>({ GroupsGradingTitle(it) }) { data, token -> GroupsGradingView(data, token) }
composable<PeerEvalGrading>({ PeerEvalsGradingTitle(it) }) { data, token -> PeerEvalsGradingView(data, token) } composable<SoloGrading>({ SolosGradingTitle(it) }) { data, token -> SolosGradingView(data, token) }
composable<PeerEvalGrading>({ PeerEvalsGradingTitle(it) }) { data, token -> PeerEvalsGradingView(data, token) }
}
} }
} }
} }

View File

@@ -4,14 +4,11 @@ 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.material3.Button
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.TimeInput import androidx.compose.material3.TimeInput
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState import androidx.compose.material3.rememberTimePickerState
@@ -70,13 +67,13 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
} }
} }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it ->
QuickAssignment(idx, it, vm) QuickAssignment(idx, it, vm)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (assignment == null) { if (assignment == null) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Text("Select an assignment to see details.", Modifier.padding(10.dp).align(Alignment.Center), fontStyle = FontStyle.Italic) Text("Select an assignment to see details.", Modifier.padding(10.dp).align(Alignment.Center), fontStyle = FontStyle.Italic)
@@ -90,7 +87,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic)
Row { Row {
Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically)) Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.clickable { updatingGrade = true }.padding(3.dp)) { Box(Modifier.clickable { updatingGrade = true }.padding(3.dp)) {
Text(when(val t = assignment.global.gradeType){ Text(when(val t = assignment.global.gradeType){
is UiGradeType.Categoric -> t.grade.name is UiGradeType.Categoric -> t.grade.name
@@ -105,7 +102,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
peerEvalData?.let { pe -> peerEvalData?.let { pe ->
Row { Row {
Text("Students are reviewing each other using ", Modifier.align(Alignment.CenterVertically)) Text("Students are reviewing each other using ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.clickable { updatingPeerEvalGrade = true }.padding(3.dp)) { Box(Modifier.clickable { updatingPeerEvalGrade = true }.padding(3.dp)) {
Text( Text(
when (val t = pe.second) { when (val t = pe.second) {
@@ -142,7 +139,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Row { Row {
Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle) Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ addingRubric = true }) { IconButton({ addingRubric = true }) {
Icon(CirclePlus, "Add grading rubric") Icon(Icons.CirclePlus, "Add grading rubric")
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
@@ -154,7 +151,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Text(it.criterion.desc, Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text(it.criterion.desc, Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
} }
IconButton({ editingRubric = idx }, Modifier.align(Alignment.Top)) { IconButton({ editingRubric = idx }, Modifier.align(Alignment.Top)) {
Icon(Edit, "Edit grading rubric") Icon(Icons.Edit, "Edit grading rubric")
} }
} }
} }
@@ -220,7 +217,7 @@ val fmt = LocalDateTime.Format {
@Composable @Composable
fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) { fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) {
val focus by vm.focusIndex val focus by vm.focusIndex
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = focus == idx, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
Text(assignment.assignment.name, fontWeight = FontWeight.Bold) Text(assignment.assignment.name, fontWeight = FontWeight.Bold)
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
@@ -275,7 +272,7 @@ fun DeadlinePicker(deadline: LocalDateTime, onDismiss: () -> Unit, onSave: (Loca
} }
Dialog(onDismiss, DialogProperties()) { Dialog(onDismiss, DialogProperties()) {
Surface(tonalElevation = 5.dp, shape = MaterialTheme.shapes.extraLarge) { Surface(shape = JewelTheme.shapes.large) {
Column(Modifier.padding(15.dp)) { Column(Modifier.padding(15.dp)) {
DatePicker(state, Modifier.fillMaxWidth()) DatePicker(state, Modifier.fillMaxWidth())
TimeInput(time, Modifier.fillMaxWidth()) TimeInput(time, Modifier.fillMaxWidth())
@@ -311,7 +308,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken:
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Criterion Name") }, isError = name in taken, singleLine = true) OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Criterion Name") }, isError = name in taken, singleLine = true)
OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true) OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true)
Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Surface(shape = JewelTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
@@ -342,7 +339,7 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
Text("Select a grading scale for $name", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(bottom = 10.dp)) Text("Select a grading scale for $name", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(bottom = 10.dp))
Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Surface(shape = JewelTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
@@ -410,8 +407,8 @@ fun GradeTypePicker(
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(categories) { idx, it -> itemsIndexed(categories) { idx, it ->
Surface( Surface(
tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp, markFocused = selectedCategory == idx,
shape = MaterialTheme.shapes.small shape = JewelTheme.shapes.small
) { ) {
Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
Text(it.grade.name, fontWeight = FontWeight.Bold) Text(it.grade.name, fontWeight = FontWeight.Bold)
@@ -434,8 +431,8 @@ fun GradeTypePicker(
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(numeric) { idx, it -> itemsIndexed(numeric) { idx, it ->
Surface( Surface(
tonalElevation = if (selectedNumeric == idx) 15.dp else 0.dp, markFocused = selectedNumeric == idx,
shape = MaterialTheme.shapes.small shape = JewelTheme.shapes.small
) { ) {
Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) {
Text(it.grade.name, fontWeight = FontWeight.Bold) Text(it.grade.name, fontWeight = FontWeight.Bold)
@@ -489,7 +486,7 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
Row(Modifier.fillMaxWidth().padding(5.dp)) { Row(Modifier.fillMaxWidth().padding(5.dp)) {
Text(it, Modifier.weight(1f)) Text(it, Modifier.weight(1f))
IconButton({ options = options.filterNot { o -> o == it } }) { IconButton({ options = options.filterNot { o -> o == it } }) {
Icon(Delete, "Delete grading option") Icon(Icons.Delete, "Delete grading option")
} }
} }
} }

View File

@@ -31,7 +31,7 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
Row { Row {
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle) Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ adding = true }) { IconButton({ adding = true }) {
Icon(CirclePlus, "Add ${tab.addText}") Icon(Icons.CirclePlus, "Add ${tab.addText}")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Add ${tab.addText}") Text("Add ${tab.addText}")
} }
@@ -65,21 +65,21 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
@Composable @Composable
fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserIcon, "Students") Icon(Icons.UserIcon, "Students")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Students") Text("Students")
} }
@Composable @Composable
fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserGroupIcon, "Groups") Icon(Icons.UserGroupIcon, "Groups")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Groups") Text("Groups")
} }
@Composable @Composable
fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(AssignmentIcon, "Assignments") Icon(Icons.AssignmentIcon, "Assignments")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Assignments") Text("Assignments")
} }

View File

@@ -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.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
//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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -43,13 +40,13 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}") Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) { Row(Modifier.fillMaxSize()) {
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it) QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (focus == -1 || selectedGroup == null) { if (focus == -1 || selectedGroup == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to start grading.", Modifier.align(Alignment.Center)) Text("Select a group to start grading.", Modifier.align(Alignment.Center))
@@ -58,13 +55,13 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Row { Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) { IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group") Icon(Icons.DoubleBack, "Previous group")
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle) Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
Icon(DoubleForward, "Next group") Icon(Icons.DoubleForward, "Next group")
} }
} }
@@ -73,7 +70,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
val global by vm.globalGrade.entity val global by vm.globalGrade.entity
val byCriteria by vm.gradeList.entities val byCriteria by vm.gradeList.entities
Surface(Modifier.fillMaxSize(), color = Color.White, shape = MaterialTheme.shapes.medium) { Surface(Modifier.fillMaxSize(), color = Color.White, shape = JewelTheme.shapes.medium) {
LazyColumn { LazyColumn {
items(byCriteria ?: listOf()) { (crit, fdbk) -> items(byCriteria ?: listOf()) { (crit, fdbk) ->
var isOpen by remember(selectedGroup) { mutableStateOf(false) } var isOpen by remember(selectedGroup) { mutableStateOf(false) }
@@ -107,7 +104,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
@Composable @Composable
fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.GroupData) { fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.GroupData) {
Surface(tonalElevation = if(isFocus) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = isFocus, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { onFocus() }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { onFocus() }.padding(10.dp)) {
Text(group.group.name, fontWeight = FontWeight.Bold) Text(group.group.name, fontWeight = FontWeight.Bold)
Text("${group.students.size} student(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text("${group.students.size} student(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
@@ -120,11 +117,11 @@ fun GFWidget(
crit: CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any, crit: CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any,
isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(), isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(),
onToggle: () -> Unit onToggle: () -> Unit
) = Surface(Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) { ) = Surface(Modifier.fillMaxWidth(), shape = JewelTheme.shapes.medium) {
Column { Column {
Surface(tonalElevation = 5.dp) { Surface {
Row(Modifier.fillMaxWidth().clickable { onToggle() }.padding(10.dp)) { Row(Modifier.fillMaxWidth().clickable { onToggle() }.padding(10.dp)) {
Icon(if(isOpen) ChevronDown else ChevronRight, "Toggle criterion detail grading", Modifier.align(Alignment.CenterVertically)) Icon(if(isOpen) Icons.ChevronDown else Icons.ChevronRight, "Toggle criterion detail grading", Modifier.align(Alignment.CenterVertically))
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Column(Modifier.align(Alignment.CenterVertically)) { Column(Modifier.align(Alignment.CenterVertically)) {
Row { Row {
@@ -165,7 +162,7 @@ fun GFWidget(
feedback.groupLevel?.let { groupLevel -> feedback.groupLevel?.let { groupLevel ->
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), tonalElevation = 10.dp, shape = MaterialTheme.shapes.small) { Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), shape = JewelTheme.shapes.small) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Text("Individual overrides", style = JewelTheme.typography.h4TextStyle) Text("Individual overrides", style = JewelTheme.typography.h4TextStyle)
feedback.overrides.forEach { (student, it) -> feedback.overrides.forEach { (student, it) ->
@@ -187,7 +184,7 @@ fun GFWidget(
if(enable) Row { if(enable) Row {
Spacer(Modifier.width(15.dp)) Spacer(Modifier.width(15.dp))
Surface(color = Color.White, shape = MaterialTheme.shapes.small) { Surface(color = Color.White, shape = JewelTheme.shapes.small) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
GradePicker(sGrade, key = crit to gr app student) { sGrade = it } GradePicker(sGrade, key = crit to gr app student) { sGrade = it }

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -19,13 +20,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@@ -66,13 +64,13 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val grades by vm.groupGrades.entities val grades by vm.groupGrades.entities
val snacks = viewModel<SnackVM> { SnackVM() } val snacks = viewModel<SnackVM> { SnackVM() }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickGroup(idx, it, vm) QuickGroup(idx, it, vm)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if(group == null) { if(group == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to view details.", Modifier.align(Alignment.Center)) Text("Select a group to view details.", Modifier.align(Alignment.Center))
@@ -84,7 +82,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Text(group.group.name, style = JewelTheme.typography.h2TextStyle) Text(group.group.name, style = JewelTheme.typography.h2TextStyle)
if (group.members.any { it.first.contact.isNotBlank() }) { if (group.members.any { it.first.contact.isNotBlank() }) {
IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) { IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) {
Icon(Mail, "Send email", Modifier.fillMaxHeight()) Icon(Icons.Mail, "Send email", Modifier.fillMaxHeight())
} }
} }
} }
@@ -101,10 +99,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Surface( Surface(
Modifier.weight(0.5f).then(if(showTargetBorder) Modifier.border(BorderStroke(3.dp, Color.Black)) else Modifier) Modifier.weight(0.5f).then(if(showTargetBorder) Modifier.border(BorderStroke(3.dp, Color.Black)) else Modifier)
.dragAndDropTarget({ true }, target = ddTarget), .dragAndDropTarget({ true }, target = ddTarget),
shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) { shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn { LazyColumn {
item { item {
Surface(tonalElevation = 15.dp) { Surface {
Row(Modifier.fillMaxWidth().padding(10.dp)) { Row(Modifier.fillMaxWidth().padding(10.dp)) {
Text("Members", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp)) Text("Members", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp))
} }
@@ -120,7 +118,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
else Text(student.contact) else Text(student.contact)
} }
if(role != null) { if(role != null) {
Surface(Modifier.align(Alignment.CenterVertically), tonalElevation = 5.dp, shape = MaterialTheme.shapes.small) { Surface(Modifier.align(Alignment.CenterVertically), shape = JewelTheme.shapes.small) {
Box(Modifier.clickable { swappingRole = -1 }.clickable { swappingRole = idx }) { Box(Modifier.clickable { swappingRole = -1 }.clickable { swappingRole = idx }) {
Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = JewelTheme.typography.regular) Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = JewelTheme.typography.regular)
} }
@@ -130,7 +128,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Text("No role", Modifier.align(Alignment.CenterVertically).clickable { swappingRole = idx }, fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f)) Text("No role", Modifier.align(Alignment.CenterVertically).clickable { swappingRole = idx }, fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
} }
IconButton({ vm.rmStudentFromGroup(student, group.group) }, Modifier.align(Alignment.CenterVertically)) { IconButton({ vm.rmStudentFromGroup(student, group.group) }, Modifier.align(Alignment.CenterVertically)) {
Icon(PersonMinus, "Remove ${student.name} from group") Icon(Icons.PersonMinus, "Remove ${student.name} from group")
} }
} }
} }
@@ -149,10 +147,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Column(Modifier.weight(0.5f)) { Column(Modifier.weight(0.5f)) {
Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle) Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) { Surface(shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn(Modifier.fillMaxHeight()) { LazyColumn(Modifier.fillMaxHeight()) {
item { item {
Surface(tonalElevation = 15.dp) { Surface {
Row(Modifier.padding(10.dp)) { Row(Modifier.padding(10.dp)) {
Text("Assignment", Modifier.weight(0.66f)) Text("Assignment", Modifier.weight(0.66f))
Text("Grade", Modifier.weight(0.33f)) Text("Grade", Modifier.weight(0.33f))
@@ -185,10 +183,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
val available by vm.groupAvailableStudents.entities val available by vm.groupAvailableStudents.entities
Surface(Modifier.weight(0.25f), shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) { Surface(Modifier.weight(0.25f), shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn { LazyColumn {
item { item {
Surface(tonalElevation = 15.dp) { Surface {
Row(Modifier.fillMaxWidth().padding(10.dp)) { Row(Modifier.fillMaxWidth().padding(10.dp)) {
Text("Available Students", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp)) Text("Available Students", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp))
} }
@@ -268,7 +266,7 @@ private class DDTarget<T>(val onStart: () -> Unit, val onEnd: () -> Unit, val va
@Composable @Composable
fun QuickGroup(idx: Int, group: EditionVM.GroupData, vm: EditionVM) { fun QuickGroup(idx: Int, group: EditionVM.GroupData, vm: EditionVM) {
val focus by vm.focusIndex val focus by vm.focusIndex
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = focus == idx, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
Text(group.group.name, fontWeight = FontWeight.Bold) Text(group.group.name, fontWeight = FontWeight.Bold)
Text("${group.members.size} member(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text("${group.members.size} member(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
@@ -291,7 +289,12 @@ fun AvailableStudent(student: Student, group: Group, vm: EditionVM) {
}) { }) {
Text(student.name, Modifier.align(Alignment.CenterVertically).weight(1f), fontWeight = FontWeight.Bold) Text(student.name, Modifier.align(Alignment.CenterVertically).weight(1f), fontWeight = FontWeight.Bold)
IconButton({ vm.addStudentToGroup(student, group, null) }) { IconButton({ vm.addStudentToGroup(student, group, null) }) {
Icon(CirclePlus, "Add ${student.name} to group") Icon(Icons.CirclePlus, "Add ${student.name} to group")
} }
} }
} }
@Composable
fun Button(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable RowScope.() -> Unit) = DefaultButton(onClick, modifier, enabled) {
Row { content() }
}

View File

@@ -4,8 +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.items import androidx.compose.foundation.lazy.items
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -33,7 +31,7 @@ fun HomeView(token: Navigator.NavToken) {
Row { Row {
Text("Courses Overview", Modifier.weight(0.8f), style = JewelTheme.typography.h2TextStyle) Text("Courses Overview", Modifier.weight(0.8f), style = JewelTheme.typography.h2TextStyle)
DefaultButton({ addingCourse = true }) { DefaultButton({ addingCourse = true }) {
Icon(CirclePlus, "Add course") Icon(Icons.CirclePlus, "Add course")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Add course") Text("Add course")
} }
@@ -56,17 +54,17 @@ fun HomeView(token: Navigator.NavToken) {
fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) { fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) {
var addingEdition by remember { mutableStateOf(false) } var addingEdition by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(false) } var deleting by remember { mutableStateOf(false) }
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.fillMaxWidth().padding(10.dp)) { Surface(shape = JewelTheme.shapes.medium, modifier = Modifier.fillMaxWidth().padding(10.dp)) {
Column(Modifier.padding(8.dp)) { Column(Modifier.padding(8.dp)) {
Row { Row {
Text(course.course.name, style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f)) Text(course.course.name, style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f))
IconButton({ deleting = true }) { Icon(Delete, "Delete course") } IconButton({ deleting = true }) { Icon(Icons.Delete, "Delete course") }
} }
Row { Row {
Text("Editions", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f)) Text("Editions", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f))
DefaultButton({ addingEdition = true }) { DefaultButton({ addingEdition = true }) {
Icon(CirclePlus, "Add edition") Icon(Icons.CirclePlus, "Add edition")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Add edition") Text("Add edition")
} }
@@ -103,7 +101,7 @@ fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onO
val type = if(edition.edition.archived) "Archived" else "Active" val type = if(edition.edition.archived) "Archived" else "Active"
var deleting by remember { mutableStateOf(false) } var deleting by remember { mutableStateOf(false) }
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.padding(10.dp).clickable { onOpen(edition.edition) }) { Surface(shape = JewelTheme.shapes.medium, modifier = Modifier.padding(10.dp).clickable { onOpen(edition.edition) }) {
Column(Modifier.padding(10.dp).width(IntrinsicSize.Min)) { Column(Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
Column(Modifier.width(IntrinsicSize.Max)) { Column(Modifier.width(IntrinsicSize.Max)) {
Text(edition.edition.name, style = JewelTheme.typography.h2TextStyle) Text(edition.edition.name, style = JewelTheme.typography.h2TextStyle)
@@ -117,21 +115,21 @@ fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onO
Row { Row {
if(edition.edition.archived) { if(edition.edition.archived) {
DefaultButton({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) { DefaultButton({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Unarchive, "Unarchive edition") Icon(Icons.Unarchive, "Unarchive edition")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Unarchive edition") Text("Unarchive edition")
} }
} }
else { else {
DefaultButton({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) { DefaultButton({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Archive, "Archive edition") Icon(Icons.Archive, "Archive edition")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Archive edition") Text("Archive edition")
} }
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
DefaultButton({ deleting = true }, Modifier.weight(0.5f)) { DefaultButton({ deleting = true }, Modifier.weight(0.5f)) {
Icon(Delete, "Archive edition") Icon(Icons.Delete, "Archive edition")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Delete edition") Text("Delete edition")
} }

View File

@@ -9,8 +9,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
val ChevronRight: ImageVector by lazy { fun ChevronRight(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "ChevronRight", name = "ChevronRight",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -20,7 +19,7 @@ val ChevronRight: ImageVector by lazy {
path( path(
fill = null, fill = null,
fillAlpha = 1.0f, fillAlpha = 1.0f,
stroke = SolidColor(Color(0xFF000000)), stroke = SolidColor(content),
strokeAlpha = 1.0f, strokeAlpha = 1.0f,
strokeLineWidth = 2f, strokeLineWidth = 2f,
strokeLineCap = StrokeCap.Round, strokeLineCap = StrokeCap.Round,
@@ -33,10 +32,8 @@ val ChevronRight: ImageVector by lazy {
lineToRelative(-6f, -6f) lineToRelative(-6f, -6f)
} }
}.build() }.build()
}
val ChevronDown: ImageVector by lazy { fun ChevronDown(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "ChevronDown", name = "ChevronDown",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -59,10 +56,8 @@ val ChevronDown: ImageVector by lazy {
lineToRelative(6f, -6f) lineToRelative(6f, -6f)
} }
}.build() }.build()
}
val ChevronLeft: ImageVector by lazy { fun ChevronLeft(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "ChevronLeft", name = "ChevronLeft",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -85,10 +80,8 @@ val ChevronLeft: ImageVector by lazy {
lineToRelative(6f, -6f) lineToRelative(6f, -6f)
} }
}.build() }.build()
}
val Delete: ImageVector by lazy { fun Delete(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "delete", name = "delete",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -140,10 +133,8 @@ val Delete: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val CirclePlus: ImageVector by lazy { fun CirclePlus(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "circle-plus", name = "circle-plus",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -184,10 +175,8 @@ val CirclePlus: ImageVector by lazy {
verticalLineToRelative(8f) verticalLineToRelative(8f)
} }
}.build() }.build()
}
val LibraryPlus: ImageVector by lazy { fun LibraryPlus(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "library-plus", name = "library-plus",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -247,10 +236,8 @@ val LibraryPlus: ImageVector by lazy {
verticalLineToRelative(6f) verticalLineToRelative(6f)
} }
}.build() }.build()
}
val Archive: ImageVector by lazy { fun Archive(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "archive", name = "archive",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -307,10 +294,8 @@ val Archive: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val Unarchive: ImageVector by lazy { fun Unarchive(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "unarchive", name = "unarchive",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -366,10 +351,8 @@ val Unarchive: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatSize: ImageVector by lazy { fun FormatSize(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "format_size", name = "format_size",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -411,10 +394,8 @@ val FormatSize: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val CircleFilled: ImageVector by lazy { fun CircleFilled(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "circle-large-filled", name = "circle-large-filled",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -452,10 +433,8 @@ val CircleFilled: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val CircleOutline: ImageVector by lazy { fun CircleOutline(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "circle-large", name = "circle-large",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -537,10 +516,8 @@ val CircleOutline: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatListBullet: ImageVector by lazy { fun FormatListBullet(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "format_list_bulleted", name = "format_list_bulleted",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -598,10 +575,8 @@ val FormatListBullet: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatListNumber: ImageVector by lazy { fun FormatListNumber(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "format_list_numbered", name = "format_list_numbered",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -675,10 +650,8 @@ val FormatListNumber: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatCode: ImageVector by lazy { fun FormatCode(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "code", name = "code",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -726,10 +699,8 @@ val FormatCode: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val ContentCopy: ImageVector by lazy { fun ContentCopy(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "content_copy", name = "content_copy",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -776,10 +747,8 @@ val ContentCopy: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val ContentPaste: ImageVector by lazy { fun ContentPaste(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "content_paste", name = "content_paste",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -830,10 +799,8 @@ val ContentPaste: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatItalic: ImageVector by lazy { fun FormatItalic(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "italic", name = "italic",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -867,10 +834,8 @@ val FormatItalic: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatBold: ImageVector by lazy { fun FormatBold(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "bold", name = "bold",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -910,10 +875,8 @@ val FormatBold: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatUnderline: ImageVector by lazy { fun FormatUnderline(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "underline", name = "underline",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -962,10 +925,8 @@ val FormatUnderline: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val FormatStrikethrough: ImageVector by lazy { fun FormatStrikethrough(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "strikethrough", name = "strikethrough",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1014,10 +975,8 @@ val FormatStrikethrough: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val UserIcon: ImageVector by lazy { fun UserIcon(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "user", name = "user",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1041,10 +1000,8 @@ val UserIcon: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val UserGroupIcon: ImageVector by lazy { fun UserGroupIcon(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "user-group", name = "user-group",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1090,10 +1047,8 @@ val UserGroupIcon: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val AssignmentIcon: ImageVector by lazy { fun AssignmentIcon(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "assignment", name = "assignment",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1162,10 +1117,8 @@ val AssignmentIcon: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val Edit: ImageVector by lazy { fun Edit(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "edit", name = "edit",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1204,10 +1157,8 @@ val Edit: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val Check: ImageVector by lazy { fun Check(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "check", name = "check",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1226,10 +1177,8 @@ val Check: ImageVector by lazy {
lineToRelative(-5f, -5f) lineToRelative(-5f, -5f)
} }
}.build() }.build()
}
val Close: ImageVector by lazy { fun Close(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "close", name = "close",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1262,10 +1211,8 @@ val Close: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val PersonMinus: ImageVector by lazy { fun PersonMinus(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "person-dash", name = "person-dash",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1306,10 +1253,8 @@ val PersonMinus: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val DoubleBack: ImageVector by lazy { fun DoubleBack(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "angle-double-left", name = "angle-double-left",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1347,10 +1292,8 @@ val DoubleBack: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val DoubleForward: ImageVector by lazy { fun DoubleForward(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "angle-double-right", name = "angle-double-right",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1388,10 +1331,8 @@ val DoubleForward: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}
val Mail: ImageVector by lazy { fun Mail(content: Color) = ImageVector.Builder(
ImageVector.Builder(
name = "mail", name = "mail",
defaultWidth = 24.dp, defaultWidth = 24.dp,
defaultHeight = 24.dp, defaultHeight = 24.dp,
@@ -1429,4 +1370,3 @@ val Mail: ImageVector by lazy {
close() close()
} }
}.build() }.build()
}

View File

@@ -0,0 +1,81 @@
package com.jaytux.grader.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.jewel.foundation.theme.LocalContentColor
interface IconData {
val ChevronRight: ImageVector
val ChevronDown: ImageVector
val ChevronLeft: ImageVector
val Delete: ImageVector
val CirclePlus: ImageVector
val LibraryPlus: ImageVector
val Archive: ImageVector
val Unarchive: ImageVector
val FormatSize: ImageVector
val CircleFilled: ImageVector
val CircleOutline: ImageVector
val FormatListBullet: ImageVector
val FormatListNumber: ImageVector
val FormatCode: ImageVector
val ContentCopy: ImageVector
val ContentPaste: ImageVector
val FormatItalic: ImageVector
val FormatBold: ImageVector
val FormatUnderline: ImageVector
val FormatStrikethrough: ImageVector
val UserIcon: ImageVector
val UserGroupIcon: ImageVector
val AssignmentIcon: ImageVector
val Edit: ImageVector
val Check: ImageVector
val Close: ImageVector
val PersonMinus: ImageVector
val DoubleBack: ImageVector
val DoubleForward: ImageVector
val Mail: ImageVector
private class Impl(val color: Color) : IconData {
override val ChevronRight: ImageVector by lazy { ChevronRight(color) }
override val ChevronDown: ImageVector by lazy { ChevronDown(color) }
override val ChevronLeft: ImageVector by lazy { ChevronLeft(color) }
override val Delete: ImageVector by lazy { Delete(color) }
override val CirclePlus: ImageVector by lazy { CirclePlus(color) }
override val LibraryPlus: ImageVector by lazy { LibraryPlus(color) }
override val Archive: ImageVector by lazy { Archive(color) }
override val Unarchive: ImageVector by lazy { Unarchive(color) }
override val FormatSize: ImageVector by lazy { FormatSize(color) }
override val CircleFilled: ImageVector by lazy { CircleFilled(color) }
override val CircleOutline: ImageVector by lazy { CircleOutline(color) }
override val FormatListBullet: ImageVector by lazy { FormatListBullet(color) }
override val FormatListNumber: ImageVector by lazy { FormatListNumber(color) }
override val FormatCode: ImageVector by lazy { FormatCode(color) }
override val ContentCopy: ImageVector by lazy { ContentCopy(color) }
override val ContentPaste: ImageVector by lazy { ContentPaste(color) }
override val FormatItalic: ImageVector by lazy { FormatItalic(color) }
override val FormatBold: ImageVector by lazy { FormatBold(color) }
override val FormatUnderline: ImageVector by lazy { FormatUnderline(color) }
override val FormatStrikethrough: ImageVector by lazy { FormatStrikethrough(color) }
override val UserIcon: ImageVector by lazy { UserIcon(color) }
override val UserGroupIcon: ImageVector by lazy { UserGroupIcon(color) }
override val AssignmentIcon: ImageVector by lazy { AssignmentIcon(color) }
override val Edit: ImageVector by lazy { Edit(color) }
override val Check: ImageVector by lazy { Check(color) }
override val Close: ImageVector by lazy { Close(color) }
override val PersonMinus: ImageVector by lazy { PersonMinus(color) }
override val DoubleBack: ImageVector by lazy { DoubleBack(color) }
override val DoubleForward: ImageVector by lazy { DoubleForward(color) }
override val Mail: ImageVector by lazy { Mail(color) }
}
companion object {
private val _cache = mutableMapOf<Color, Impl>()
operator fun get(color: Color): IconData = _cache.getOrPut(color) { Impl(color) }
}
}
@get:Composable
val Icons: IconData
get() = IconData[LocalContentColor.current]

View File

@@ -4,9 +4,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -58,13 +56,13 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}") Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) { Row(Modifier.fillMaxSize()) {
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it) QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (focus == -1 || selectedGroup == null) { if (focus == -1 || selectedGroup == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to start grading.", Modifier.align(Alignment.Center)) Text("Select a group to start grading.", Modifier.align(Alignment.Center))
@@ -73,13 +71,13 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Row { Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) { IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group") Icon(Icons.DoubleBack, "Previous group")
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle) Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
Icon(DoubleForward, "Next group") Icon(Icons.DoubleForward, "Next group")
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
@@ -90,7 +88,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
} }
} }
} ?: Box(Modifier.weight(0.66f).fillMaxWidth()) { } ?: Box(Modifier.weight(0.66f).fillMaxWidth()) {
Text("Error: could not load evaluations for this group.", Modifier.align(Alignment.Center), color = MaterialTheme.colorScheme.error) Text("Error: could not load evaluations for this group.", Modifier.align(Alignment.Center), color = JewelTheme.globalColors.text.error)
} }
Column(Modifier.weight(0.33f)) { Column(Modifier.weight(0.33f)) {
@@ -101,7 +99,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
sgs.forEachIndexed { idx, st -> sgs.forEachIndexed { idx, st ->
Tab(idx == selectedStudent, { selectedStudent = idx }) { Tab(idx == selectedStudent, { selectedStudent = idx }) {
Row { Row {
Icon(UserIcon, "") Icon(Icons.UserIcon, "")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text(st.first.name, Modifier.align(Alignment.CenterVertically)) Text(st.first.name, Modifier.align(Alignment.CenterVertically))
} }
@@ -221,7 +219,7 @@ fun GradeTable(
} }
editing?.let { editing?.let {
Surface(Modifier.weight(0.33f), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.weight(0.33f), shape = JewelTheme.shapes.medium) {
val (evaluator, evaluatee, data) = it val (evaluator, evaluatee, data) = it
EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { grade, feedback -> EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { grade, feedback ->
onSet(evaluator, evaluatee, group, grade, feedback) onSet(evaluator, evaluatee, group, grade, feedback)

View File

@@ -4,8 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
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.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -25,8 +23,11 @@ import com.jaytux.grader.toClipboard
import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalContentColor import org.jetbrains.jewel.foundation.theme.LocalContentColor
import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
import org.jetbrains.jewel.ui.theme.iconButtonStyle
@Composable @Composable
fun RichTextStyleRow( fun RichTextStyleRow(
@@ -51,7 +52,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold, isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
icon = FormatBold icon = Icons.FormatBold
) )
} }
@@ -65,7 +66,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic, isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic,
icon = FormatItalic icon = Icons.FormatItalic
) )
} }
@@ -79,7 +80,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true,
icon = FormatUnderline icon = Icons.FormatUnderline
) )
} }
@@ -93,7 +94,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true,
icon = FormatStrikethrough icon = Icons.FormatStrikethrough
) )
} }
@@ -107,7 +108,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontSize == 28.sp, isSelected = state.currentSpanStyle.fontSize == 28.sp,
icon = FormatSize icon = Icons.FormatSize
) )
} }
@@ -121,7 +122,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.color == Color.Red, isSelected = state.currentSpanStyle.color == Color.Red,
icon = CircleFilled, icon = Icons.CircleFilled,
tint = Color.Red tint = Color.Red
) )
} }
@@ -136,7 +137,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.background == Color.Yellow, isSelected = state.currentSpanStyle.background == Color.Yellow,
icon = CircleOutline, icon = Icons.CircleOutline,
tint = Color.Yellow tint = Color.Yellow
) )
} }
@@ -156,7 +157,7 @@ fun RichTextStyleRow(
state.toggleUnorderedList() state.toggleUnorderedList()
}, },
isSelected = state.isUnorderedList, isSelected = state.isUnorderedList,
icon = FormatListBullet, icon = Icons.FormatListBullet,
) )
} }
@@ -166,7 +167,7 @@ fun RichTextStyleRow(
state.toggleOrderedList() state.toggleOrderedList()
}, },
isSelected = state.isOrderedList, isSelected = state.isOrderedList,
icon = FormatListNumber, icon = Icons.FormatListNumber,
) )
} }
@@ -185,16 +186,16 @@ fun RichTextStyleRow(
state.toggleCodeSpan() state.toggleCodeSpan()
}, },
isSelected = state.isCodeSpan, isSelected = state.isCodeSpan,
icon = FormatCode, icon = Icons.FormatCode,
) )
} }
} }
IconButton({ scope.launch { state.toClipboard(clip) } }) { IconButton({ scope.launch { state.toClipboard(clip) } }) {
Icon(ContentCopy, contentDescription = "Copy markdown") Icon(Icons.ContentCopy, contentDescription = "Copy markdown")
} }
IconButton({ scope.launch { state.loadClipboard(clip, scope) } }) { IconButton({ scope.launch { state.loadClipboard(clip, scope) } }) {
Icon(ContentPaste, contentDescription = "Paste markdown") Icon(Icons.ContentPaste, contentDescription = "Paste markdown")
} }
} }
} }
@@ -213,13 +214,7 @@ fun RichTextStyleButton(
// (Happens only on Desktop) // (Happens only on Desktop)
.focusProperties { canFocus = false }, .focusProperties { canFocus = false },
onClick = onClick, onClick = onClick,
// colors = IconButtonDefaults.iconButtonColors( style = IconButtonStyle(JewelTheme.iconButtonStyle.colors, JewelTheme.iconButtonStyle.metrics) // TODO: color swapping depending on isSelected
// contentColor = if (isSelected) {
// MaterialTheme.colorScheme.onPrimary
// } else {
// MaterialTheme.colorScheme.onBackground
// },
// ),
) { ) {
Icon( Icon(
icon, icon,
@@ -228,7 +223,7 @@ fun RichTextStyleButton(
modifier = Modifier modifier = Modifier
.background( .background(
color = if (isSelected) { color = if (isSelected) {
MaterialTheme.colorScheme.primary JewelTheme.globalColors.text.disabledSelected
} else { } else {
Color.Transparent Color.Transparent
}, },

View File

@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -45,13 +43,13 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val focus by vm.focusIndex val focus by vm.focusIndex
val snacks = viewModel<SnackVM> { SnackVM() } val snacks = viewModel<SnackVM> { SnackVM() }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(students, { Text("No students yet.") }) { idx, it -> ListOrEmpty(students, { Text("No students yet.") }) { idx, it ->
QuickStudent(idx, it, vm) QuickStudent(idx, it, vm)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if(focus == -1) { if(focus == -1) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a student to view details.", Modifier.align(Alignment.Center)) Text("Select a student to view details.", Modifier.align(Alignment.Center))
@@ -62,13 +60,13 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val grades by vm.studentGrades.entities val grades by vm.studentGrades.entities
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.padding(10.dp).fillMaxWidth(), shape = JewelTheme.shapes.medium) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = JewelTheme.typography.h2TextStyle) Text(students[focus].name, style = JewelTheme.typography.h2TextStyle)
if(students[focus].contact.isNotBlank()) { if(students[focus].contact.isNotBlank()) {
IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) { IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) {
Icon(Mail, "Send email", Modifier.fillMaxHeight()) Icon(Icons.Mail, "Send email", Modifier.fillMaxHeight())
} }
} }
} }
@@ -89,18 +87,18 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Text(students[focus].contact, Modifier.padding(start = 5.dp)) Text(students[focus].contact, Modifier.padding(start = 5.dp))
} }
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Edit, "Edit contact info", Modifier.clickable { editing = true }) Icon(Icons.Edit, "Edit contact info", Modifier.clickable { editing = true })
} }
else { else {
var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) } var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) }
OutlinedTextField(mod, { mod = it }) OutlinedTextField(mod, { mod = it })
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable { Icon(Icons.Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable {
vm.modStudent(students[focus], null, mod, null) vm.modStudent(students[focus], null, mod, null)
editing = false editing = false
}) })
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false }) Icon(Icons.Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false })
} }
} }
@@ -111,7 +109,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
else { else {
FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) { FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
gList.forEach { group -> gList.forEach { group ->
Surface(tonalElevation = 15.dp, shadowElevation = 1.dp, shape = MaterialTheme.shapes.small) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.padding(5.dp).clickable { vm.focus(group.first) }) { Box(Modifier.padding(5.dp).clickable { vm.focus(group.first) }) {
Text("${group.first.name} (${group.second ?: "no role"})", Modifier.padding(5.dp)) Text("${group.first.name} (${group.second ?: "no role"})", Modifier.padding(5.dp))
} }
@@ -147,10 +145,10 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Column(Modifier.weight(0.66f)) { Column(Modifier.weight(0.66f)) {
Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle) Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) { Surface(shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn { LazyColumn {
item { item {
Surface(tonalElevation = 15.dp) { Surface {
Row(Modifier.padding(10.dp)) { Row(Modifier.padding(10.dp)) {
Text("Assignment", Modifier.weight(0.66f)) Text("Assignment", Modifier.weight(0.66f))
Text("Grade", Modifier.weight(0.33f)) Text("Grade", Modifier.weight(0.33f))
@@ -194,7 +192,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@Composable @Composable
fun QuickStudent(idx: Int, student: Student, vm: EditionVM) { fun QuickStudent(idx: Int, student: Student, vm: EditionVM) {
val focus by vm.focusIndex val focus by vm.focusIndex
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = focus == idx, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
Text(student.name, fontWeight = FontWeight.Bold) Text(student.name, fontWeight = FontWeight.Bold)
if(student.contact.isBlank()) if(student.contact.isBlank())

View File

@@ -1,25 +1,32 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable 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.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
@@ -32,10 +39,15 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.maxN import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.Grade import com.jaytux.grader.viewmodel.Grade
import kotlinx.datetime.* import kotlinx.datetime.*
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import java.util.* import java.util.*
import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalTextStyle
import org.jetbrains.jewel.ui.Outline import org.jetbrains.jewel.ui.Outline
import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.theme.colorPalette
import org.jetbrains.jewel.ui.theme.iconData
import org.jetbrains.jewel.ui.typography import org.jetbrains.jewel.ui.typography
@Composable @Composable
@@ -80,7 +92,7 @@ fun ConfirmDeleteDialog(
onCloseRequest = onExit, onCloseRequest = onExit,
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center)) state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
) { ) {
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) { Surface(Modifier.width(400.dp).height(300.dp)) {
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp)) Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp))
@@ -153,8 +165,8 @@ fun Selectable(
) { ) {
Surface( Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) selectedElevation else unselectedElevation, markFocused = isSelected,
shape = MaterialTheme.shapes.medium shape = JewelTheme.shapes.medium
) { ) {
content() content()
} }
@@ -197,7 +209,7 @@ fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (
Text("Used roles:") Text("Used roles:")
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) { LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
items(used) { items(used) {
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) { Surface(Modifier.fillMaxWidth().clickable { role = it }) {
Text(it, Modifier.padding(5.dp)) Text(it, Modifier.padding(5.dp))
} }
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
@@ -284,8 +296,6 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
} }
} }
// TextField(true, name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text(label) }, isError = name in taken)
@Composable @Composable
fun OutlinedTextField(value: String, onChange: (String) -> Unit, modifier: Modifier = Modifier, label: @Composable () -> Unit = {}, isError: Boolean = false, singleLine: Boolean = false, minLines: Int = 1) { fun OutlinedTextField(value: String, onChange: (String) -> Unit, modifier: Modifier = Modifier, label: @Composable () -> Unit = {}, isError: Boolean = false, singleLine: Boolean = false, minLines: Int = 1) {
val state = remember { TextFieldState(value) } val state = remember { TextFieldState(value) }
@@ -314,3 +324,103 @@ fun OutlinedTextField(value: String, onChange: (String) -> Unit, modifier: Modif
TextArea(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label /*, minLines = minLines*/) TextArea(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label /*, minLines = minLines*/)
} }
} }
private val LocalSurfaceLayer = compositionLocalOf { 0 }
interface ShapeCollection {
val xLarge: Shape
val large: Shape
val medium: Shape
val small: Shape
val xSmall: Shape
val none: Shape
}
val JewelTheme.Companion.shapes
get() = object : ShapeCollection {
override val xLarge = RoundedCornerShape(28.0.dp)
override val large = RoundedCornerShape(16.0.dp)
override val xSmall = RoundedCornerShape(4.0.dp)
override val medium = RoundedCornerShape(12.0.dp)
override val none = RectangleShape
override val small = RoundedCornerShape(8.0.dp)
}
@Composable
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
color: Color = JewelTheme.globalColors.panelBackground,
markFocused: Boolean = false,
content: @Composable () -> Unit
) {
val currentLayer = LocalSurfaceLayer.current
// TODO: markFocused?
Box(modifier = modifier.background(color, shape).let {
if (currentLayer > 0) it.border(Stroke(1.dp, JewelTheme.globalColors.outlines.focused, Stroke.Alignment.Center), shape)
else it
}) {
CompositionLocalProvider(LocalSurfaceLayer provides currentLayer + 1) { content() }
}
}
@Composable
fun Scaffold(topBar: @Composable () -> Unit, snackState: SnackbarHostState, content: @Composable () -> Unit) {
Column(Modifier.fillMaxSize()) {
Box(Modifier.heightIn(max = 150.dp)) {
CompositionLocalProvider(LocalTextStyle provides JewelTheme.typography.h1TextStyle) {
topBar()
}
}
Box(Modifier.weight(1f)) {
content()
}
}
NotificationHost(snackState)
}
@Composable
fun NotificationHost(host: SnackbarHostState) {
val currentData = host.currentSnackbarData
Box(Modifier.fillMaxSize()) {
if (currentData != null) {
Notification(
message = currentData.visuals.message,
onDismiss = { currentData.dismiss() },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
)
}
}
}
@Composable
fun Notification(message: String, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.widthIn(max = 300.dp).shadow(8.dp, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp)
) {
Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = message, modifier = Modifier.weight(1f), style = JewelTheme.defaultTextStyle)
IconButton(onClick = onDismiss) {
Icon(Icons.Close, contentDescription = "Close")
}
}
}
}
@Composable
fun TitleBar(modifier: Modifier = Modifier, title: @Composable () -> Unit, navigationIcon: (@Composable () -> Unit)? = null) {
Surface(modifier) {
Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 15.dp), verticalAlignment = Alignment.CenterVertically) {
if (navigationIcon != null) {
Box(Modifier.padding(end = 8.dp)) {
navigationIcon()
}
}
title()
}
}
}

View File

@@ -1,7 +1,8 @@
package com.jaytux.grader.viewmodel package com.jaytux.grader.viewmodel
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.* import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -12,7 +13,13 @@ import androidx.compose.ui.backhandler.BackHandler
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.ui.ChevronLeft import com.jaytux.grader.ui.ChevronLeft
import com.jaytux.grader.ui.Icons
import com.jaytux.grader.ui.Scaffold
import com.jaytux.grader.ui.Surface
import com.jaytux.grader.ui.TitleBar
import org.jetbrains.jewel.foundation.theme.JewelTheme
import kotlin.reflect.KClass import kotlin.reflect.KClass
import org.jetbrains.jewel.ui.component.*;
class Navigator private constructor( class Navigator private constructor(
private var _start: IDestination, private var _start: IDestination,
@@ -56,7 +63,7 @@ class Navigator private constructor(
inline fun <reified T : IDestination> backTo() = backTo(T::class) inline fun <reified T : IDestination> backTo() = backTo(T::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun DisplayScaffold() { fun DisplayScaffold() {
val state = remember { SnackbarHostState() } val state = remember { SnackbarHostState() }
@@ -73,21 +80,18 @@ class Navigator private constructor(
BackHandler { back() } BackHandler { back() }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TitleBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer), modifier = Modifier.fillMaxWidth(),
title = { render.header(top.dest) }, title = { render.header(top.dest) },
navigationIcon = { ) {
IconButton({ back() }, enabled = top != _start) { IconButton({ back() }, enabled = top != _start) {
Icon(ChevronLeft, contentDescription = "Back") Icon(Icons.ChevronLeft, contentDescription = "Back")
}
} }
) }
}, },
snackbarHost = { snackState = state
SnackbarHost(state) ) { //insets ->
} Surface(/*Modifier.padding(insets),*/ color = JewelTheme.globalColors.panelBackground) {
) { insets ->
Surface(Modifier.padding(insets), color = MaterialTheme.colorScheme.surface) {
render.renderer(top.dest, top.token) render.renderer(top.dest, top.token)
} }
} }

View File

@@ -43,6 +43,7 @@ filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialog
filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" } filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" }
directories = { group = "dev.dirs", name = "directories", version.ref = "directories" } directories = { group = "dev.dirs", name = "directories", version.ref = "directories" }
jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone", version.ref = "jewel" } jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone", version.ref = "jewel" }
jewel-windows = { group = "org.jetbrains.jewel", name = "jewel-int-ui-decorated-window", version.ref = "jewel" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }