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.compose.backhandler)
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.SolosGradingTitle
import com.jaytux.grader.ui.SolosGradingView
import com.jaytux.grader.ui.Surface
import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
@@ -26,6 +27,7 @@ data class PeerEvalGrading(val course: Course, val edition: Edition, val assignm
@Composable
fun App() {
IntUiTheme(isDark = true) {
Surface {
Navigator.NavHost(Home) {
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }
@@ -34,4 +36,5 @@ fun App() {
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.TimeInput
import androidx.compose.material3.rememberDatePickerState
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 ->
QuickAssignment(idx, it, vm)
}
}
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (assignment == null) {
Box(Modifier.fillMaxSize()) {
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)
Row {
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)) {
Text(when(val t = assignment.global.gradeType){
is UiGradeType.Categoric -> t.grade.name
@@ -105,7 +102,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
peerEvalData?.let { pe ->
Row {
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)) {
Text(
when (val t = pe.second) {
@@ -142,7 +139,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Row {
Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ addingRubric = true }) {
Icon(CirclePlus, "Add grading rubric")
Icon(Icons.CirclePlus, "Add grading rubric")
}
}
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)
}
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
fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) {
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)) {
Text(assignment.assignment.name, fontWeight = FontWeight.Bold)
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()) {
Surface(tonalElevation = 5.dp, shape = MaterialTheme.shapes.extraLarge) {
Surface(shape = JewelTheme.shapes.large) {
Column(Modifier.padding(15.dp)) {
DatePicker(state, Modifier.fillMaxWidth())
TimeInput(time, Modifier.fillMaxWidth())
@@ -311,7 +308,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken:
Column(Modifier.align(Alignment.Center)) {
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)
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 {
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)) {
Column(Modifier.align(Alignment.Center)) {
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 {
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)) {
itemsIndexed(categories) { idx, it ->
Surface(
tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp,
shape = MaterialTheme.shapes.small
markFocused = selectedCategory == idx,
shape = JewelTheme.shapes.small
) {
Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
Text(it.grade.name, fontWeight = FontWeight.Bold)
@@ -434,8 +431,8 @@ fun GradeTypePicker(
LazyColumn(Modifier.weight(1f)) {
itemsIndexed(numeric) { idx, it ->
Surface(
tonalElevation = if (selectedNumeric == idx) 15.dp else 0.dp,
shape = MaterialTheme.shapes.small
markFocused = selectedNumeric == idx,
shape = JewelTheme.shapes.small
) {
Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) {
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)) {
Text(it, Modifier.weight(1f))
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 {
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ adding = true }) {
Icon(CirclePlus, "Add ${tab.addText}")
Icon(Icons.CirclePlus, "Add ${tab.addText}")
Spacer(Modifier.width(5.dp))
Text("Add ${tab.addText}")
}
@@ -65,21 +65,21 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
@Composable
fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserIcon, "Students")
Icon(Icons.UserIcon, "Students")
Spacer(Modifier.width(5.dp))
Text("Students")
}
@Composable
fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserGroupIcon, "Groups")
Icon(Icons.UserGroupIcon, "Groups")
Spacer(Modifier.width(5.dp))
Text("Groups")
}
@Composable
fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(AssignmentIcon, "Assignments")
Icon(Icons.AssignmentIcon, "Assignments")
Spacer(Modifier.width(5.dp))
Text("Assignments")
}

View File

@@ -4,9 +4,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
//import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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}")
Spacer(Modifier.height(5.dp))
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 ->
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) {
Box(Modifier.weight(0.75f).fillMaxHeight()) {
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)) {
Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group")
Icon(Icons.DoubleBack, "Previous group")
}
Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f))
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 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 {
items(byCriteria ?: listOf()) { (crit, fdbk) ->
var isOpen by remember(selectedGroup) { mutableStateOf(false) }
@@ -107,7 +104,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
@Composable
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)) {
Text(group.group.name, fontWeight = FontWeight.Bold)
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,
isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(),
onToggle: () -> Unit
) = Surface(Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) {
) = Surface(Modifier.fillMaxWidth(), shape = JewelTheme.shapes.medium) {
Column {
Surface(tonalElevation = 5.dp) {
Surface {
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))
Column(Modifier.align(Alignment.CenterVertically)) {
Row {
@@ -165,7 +162,7 @@ fun GFWidget(
feedback.groupLevel?.let { groupLevel ->
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)) {
Text("Individual overrides", style = JewelTheme.typography.h4TextStyle)
feedback.overrides.forEach { (student, it) ->
@@ -187,7 +184,7 @@ fun GFWidget(
if(enable) Row {
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)) {
Spacer(Modifier.height(5.dp))
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.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.items
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -66,13 +64,13 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val grades by vm.groupGrades.entities
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 ->
QuickGroup(idx, it, vm)
}
}
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if(group == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) {
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)
if (group.members.any { it.first.contact.isNotBlank() }) {
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(
Modifier.weight(0.5f).then(if(showTargetBorder) Modifier.border(BorderStroke(3.dp, Color.Black)) else Modifier)
.dragAndDropTarget({ true }, target = ddTarget),
shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) {
shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn {
item {
Surface(tonalElevation = 15.dp) {
Surface {
Row(Modifier.fillMaxWidth().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)
}
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 }) {
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))
}
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)) {
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()) {
item {
Surface(tonalElevation = 15.dp) {
Surface {
Row(Modifier.padding(10.dp)) {
Text("Assignment", Modifier.weight(0.66f))
Text("Grade", Modifier.weight(0.33f))
@@ -185,10 +183,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Spacer(Modifier.width(10.dp))
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 {
item {
Surface(tonalElevation = 15.dp) {
Surface {
Row(Modifier.fillMaxWidth().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
fun QuickGroup(idx: Int, group: EditionVM.GroupData, vm: EditionVM) {
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)) {
Text(group.group.name, fontWeight = FontWeight.Bold)
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)
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,7 +31,7 @@ fun HomeView(token: Navigator.NavToken) {
Row {
Text("Courses Overview", Modifier.weight(0.8f), style = JewelTheme.typography.h2TextStyle)
DefaultButton({ addingCourse = true }) {
Icon(CirclePlus, "Add course")
Icon(Icons.CirclePlus, "Add course")
Spacer(Modifier.width(5.dp))
Text("Add course")
}
@@ -56,17 +54,17 @@ fun HomeView(token: Navigator.NavToken) {
fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) {
var addingEdition 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)) {
Row {
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 {
Text("Editions", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f))
DefaultButton({ addingEdition = true }) {
Icon(CirclePlus, "Add edition")
Icon(Icons.CirclePlus, "Add edition")
Spacer(Modifier.width(5.dp))
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"
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.width(IntrinsicSize.Max)) {
Text(edition.edition.name, style = JewelTheme.typography.h2TextStyle)
@@ -117,21 +115,21 @@ fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onO
Row {
if(edition.edition.archived) {
DefaultButton({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Unarchive, "Unarchive edition")
Icon(Icons.Unarchive, "Unarchive edition")
Spacer(Modifier.width(5.dp))
Text("Unarchive edition")
}
}
else {
DefaultButton({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Archive, "Archive edition")
Icon(Icons.Archive, "Archive edition")
Spacer(Modifier.width(5.dp))
Text("Archive edition")
}
}
Spacer(Modifier.width(10.dp))
DefaultButton({ deleting = true }, Modifier.weight(0.5f)) {
Icon(Delete, "Archive edition")
Icon(Icons.Delete, "Archive edition")
Spacer(Modifier.width(5.dp))
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.unit.dp
val ChevronRight: ImageVector by lazy {
ImageVector.Builder(
fun ChevronRight(content: Color) = ImageVector.Builder(
name = "ChevronRight",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -20,7 +19,7 @@ val ChevronRight: ImageVector by lazy {
path(
fill = null,
fillAlpha = 1.0f,
stroke = SolidColor(Color(0xFF000000)),
stroke = SolidColor(content),
strokeAlpha = 1.0f,
strokeLineWidth = 2f,
strokeLineCap = StrokeCap.Round,
@@ -33,10 +32,8 @@ val ChevronRight: ImageVector by lazy {
lineToRelative(-6f, -6f)
}
}.build()
}
val ChevronDown: ImageVector by lazy {
ImageVector.Builder(
fun ChevronDown(content: Color) = ImageVector.Builder(
name = "ChevronDown",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -59,10 +56,8 @@ val ChevronDown: ImageVector by lazy {
lineToRelative(6f, -6f)
}
}.build()
}
val ChevronLeft: ImageVector by lazy {
ImageVector.Builder(
fun ChevronLeft(content: Color) = ImageVector.Builder(
name = "ChevronLeft",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -85,10 +80,8 @@ val ChevronLeft: ImageVector by lazy {
lineToRelative(6f, -6f)
}
}.build()
}
val Delete: ImageVector by lazy {
ImageVector.Builder(
fun Delete(content: Color) = ImageVector.Builder(
name = "delete",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -140,10 +133,8 @@ val Delete: ImageVector by lazy {
close()
}
}.build()
}
val CirclePlus: ImageVector by lazy {
ImageVector.Builder(
fun CirclePlus(content: Color) = ImageVector.Builder(
name = "circle-plus",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -184,10 +175,8 @@ val CirclePlus: ImageVector by lazy {
verticalLineToRelative(8f)
}
}.build()
}
val LibraryPlus: ImageVector by lazy {
ImageVector.Builder(
fun LibraryPlus(content: Color) = ImageVector.Builder(
name = "library-plus",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -247,10 +236,8 @@ val LibraryPlus: ImageVector by lazy {
verticalLineToRelative(6f)
}
}.build()
}
val Archive: ImageVector by lazy {
ImageVector.Builder(
fun Archive(content: Color) = ImageVector.Builder(
name = "archive",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -307,10 +294,8 @@ val Archive: ImageVector by lazy {
close()
}
}.build()
}
val Unarchive: ImageVector by lazy {
ImageVector.Builder(
fun Unarchive(content: Color) = ImageVector.Builder(
name = "unarchive",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -366,10 +351,8 @@ val Unarchive: ImageVector by lazy {
close()
}
}.build()
}
val FormatSize: ImageVector by lazy {
ImageVector.Builder(
fun FormatSize(content: Color) = ImageVector.Builder(
name = "format_size",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -411,10 +394,8 @@ val FormatSize: ImageVector by lazy {
close()
}
}.build()
}
val CircleFilled: ImageVector by lazy {
ImageVector.Builder(
fun CircleFilled(content: Color) = ImageVector.Builder(
name = "circle-large-filled",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -452,10 +433,8 @@ val CircleFilled: ImageVector by lazy {
close()
}
}.build()
}
val CircleOutline: ImageVector by lazy {
ImageVector.Builder(
fun CircleOutline(content: Color) = ImageVector.Builder(
name = "circle-large",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -537,10 +516,8 @@ val CircleOutline: ImageVector by lazy {
close()
}
}.build()
}
val FormatListBullet: ImageVector by lazy {
ImageVector.Builder(
fun FormatListBullet(content: Color) = ImageVector.Builder(
name = "format_list_bulleted",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -598,10 +575,8 @@ val FormatListBullet: ImageVector by lazy {
close()
}
}.build()
}
val FormatListNumber: ImageVector by lazy {
ImageVector.Builder(
fun FormatListNumber(content: Color) = ImageVector.Builder(
name = "format_list_numbered",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -675,10 +650,8 @@ val FormatListNumber: ImageVector by lazy {
close()
}
}.build()
}
val FormatCode: ImageVector by lazy {
ImageVector.Builder(
fun FormatCode(content: Color) = ImageVector.Builder(
name = "code",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -726,10 +699,8 @@ val FormatCode: ImageVector by lazy {
close()
}
}.build()
}
val ContentCopy: ImageVector by lazy {
ImageVector.Builder(
fun ContentCopy(content: Color) = ImageVector.Builder(
name = "content_copy",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -776,10 +747,8 @@ val ContentCopy: ImageVector by lazy {
close()
}
}.build()
}
val ContentPaste: ImageVector by lazy {
ImageVector.Builder(
fun ContentPaste(content: Color) = ImageVector.Builder(
name = "content_paste",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -830,10 +799,8 @@ val ContentPaste: ImageVector by lazy {
close()
}
}.build()
}
val FormatItalic: ImageVector by lazy {
ImageVector.Builder(
fun FormatItalic(content: Color) = ImageVector.Builder(
name = "italic",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -867,10 +834,8 @@ val FormatItalic: ImageVector by lazy {
close()
}
}.build()
}
val FormatBold: ImageVector by lazy {
ImageVector.Builder(
fun FormatBold(content: Color) = ImageVector.Builder(
name = "bold",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -910,10 +875,8 @@ val FormatBold: ImageVector by lazy {
close()
}
}.build()
}
val FormatUnderline: ImageVector by lazy {
ImageVector.Builder(
fun FormatUnderline(content: Color) = ImageVector.Builder(
name = "underline",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -962,10 +925,8 @@ val FormatUnderline: ImageVector by lazy {
close()
}
}.build()
}
val FormatStrikethrough: ImageVector by lazy {
ImageVector.Builder(
fun FormatStrikethrough(content: Color) = ImageVector.Builder(
name = "strikethrough",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1014,10 +975,8 @@ val FormatStrikethrough: ImageVector by lazy {
close()
}
}.build()
}
val UserIcon: ImageVector by lazy {
ImageVector.Builder(
fun UserIcon(content: Color) = ImageVector.Builder(
name = "user",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1041,10 +1000,8 @@ val UserIcon: ImageVector by lazy {
close()
}
}.build()
}
val UserGroupIcon: ImageVector by lazy {
ImageVector.Builder(
fun UserGroupIcon(content: Color) = ImageVector.Builder(
name = "user-group",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1090,10 +1047,8 @@ val UserGroupIcon: ImageVector by lazy {
close()
}
}.build()
}
val AssignmentIcon: ImageVector by lazy {
ImageVector.Builder(
fun AssignmentIcon(content: Color) = ImageVector.Builder(
name = "assignment",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1162,10 +1117,8 @@ val AssignmentIcon: ImageVector by lazy {
close()
}
}.build()
}
val Edit: ImageVector by lazy {
ImageVector.Builder(
fun Edit(content: Color) = ImageVector.Builder(
name = "edit",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1204,10 +1157,8 @@ val Edit: ImageVector by lazy {
close()
}
}.build()
}
val Check: ImageVector by lazy {
ImageVector.Builder(
fun Check(content: Color) = ImageVector.Builder(
name = "check",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1226,10 +1177,8 @@ val Check: ImageVector by lazy {
lineToRelative(-5f, -5f)
}
}.build()
}
val Close: ImageVector by lazy {
ImageVector.Builder(
fun Close(content: Color) = ImageVector.Builder(
name = "close",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1262,10 +1211,8 @@ val Close: ImageVector by lazy {
close()
}
}.build()
}
val PersonMinus: ImageVector by lazy {
ImageVector.Builder(
fun PersonMinus(content: Color) = ImageVector.Builder(
name = "person-dash",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1306,10 +1253,8 @@ val PersonMinus: ImageVector by lazy {
close()
}
}.build()
}
val DoubleBack: ImageVector by lazy {
ImageVector.Builder(
fun DoubleBack(content: Color) = ImageVector.Builder(
name = "angle-double-left",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1347,10 +1292,8 @@ val DoubleBack: ImageVector by lazy {
close()
}
}.build()
}
val DoubleForward: ImageVector by lazy {
ImageVector.Builder(
fun DoubleForward(content: Color) = ImageVector.Builder(
name = "angle-double-right",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1388,10 +1331,8 @@ val DoubleForward: ImageVector by lazy {
close()
}
}.build()
}
val Mail: ImageVector by lazy {
ImageVector.Builder(
fun Mail(content: Color) = ImageVector.Builder(
name = "mail",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
@@ -1429,4 +1370,3 @@ val Mail: ImageVector by lazy {
close()
}
}.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.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.runtime.*
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}")
Spacer(Modifier.height(5.dp))
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 ->
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) {
Box(Modifier.weight(0.75f).fillMaxHeight()) {
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)) {
Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group")
Icon(Icons.DoubleBack, "Previous group")
}
Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f))
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))
@@ -90,7 +88,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
}
}
} ?: 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)) {
@@ -101,7 +99,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
sgs.forEachIndexed { idx, st ->
Tab(idx == selectedStudent, { selectedStudent = idx }) {
Row {
Icon(UserIcon, "")
Icon(Icons.UserIcon, "")
Spacer(Modifier.width(5.dp))
Text(st.first.name, Modifier.align(Alignment.CenterVertically))
}
@@ -221,7 +219,7 @@ fun GradeTable(
}
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
EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { 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.lazy.LazyRow
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.rememberCoroutineScope
import androidx.compose.ui.Alignment
@@ -25,8 +23,11 @@ import com.jaytux.grader.toClipboard
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalContentColor
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
import org.jetbrains.jewel.ui.theme.iconButtonStyle
@Composable
fun RichTextStyleRow(
@@ -51,7 +52,7 @@ fun RichTextStyleRow(
)
},
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
icon = FormatBold
icon = Icons.FormatBold
)
}
@@ -65,7 +66,7 @@ fun RichTextStyleRow(
)
},
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,
icon = FormatUnderline
icon = Icons.FormatUnderline
)
}
@@ -93,7 +94,7 @@ fun RichTextStyleRow(
)
},
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,
icon = FormatSize
icon = Icons.FormatSize
)
}
@@ -121,7 +122,7 @@ fun RichTextStyleRow(
)
},
isSelected = state.currentSpanStyle.color == Color.Red,
icon = CircleFilled,
icon = Icons.CircleFilled,
tint = Color.Red
)
}
@@ -136,7 +137,7 @@ fun RichTextStyleRow(
)
},
isSelected = state.currentSpanStyle.background == Color.Yellow,
icon = CircleOutline,
icon = Icons.CircleOutline,
tint = Color.Yellow
)
}
@@ -156,7 +157,7 @@ fun RichTextStyleRow(
state.toggleUnorderedList()
},
isSelected = state.isUnorderedList,
icon = FormatListBullet,
icon = Icons.FormatListBullet,
)
}
@@ -166,7 +167,7 @@ fun RichTextStyleRow(
state.toggleOrderedList()
},
isSelected = state.isOrderedList,
icon = FormatListNumber,
icon = Icons.FormatListNumber,
)
}
@@ -185,16 +186,16 @@ fun RichTextStyleRow(
state.toggleCodeSpan()
},
isSelected = state.isCodeSpan,
icon = FormatCode,
icon = Icons.FormatCode,
)
}
}
IconButton({ scope.launch { state.toClipboard(clip) } }) {
Icon(ContentCopy, contentDescription = "Copy markdown")
Icon(Icons.ContentCopy, contentDescription = "Copy markdown")
}
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)
.focusProperties { canFocus = false },
onClick = onClick,
// colors = IconButtonDefaults.iconButtonColors(
// contentColor = if (isSelected) {
// MaterialTheme.colorScheme.onPrimary
// } else {
// MaterialTheme.colorScheme.onBackground
// },
// ),
style = IconButtonStyle(JewelTheme.iconButtonStyle.colors, JewelTheme.iconButtonStyle.metrics) // TODO: color swapping depending on isSelected
) {
Icon(
icon,
@@ -228,7 +223,7 @@ fun RichTextStyleButton(
modifier = Modifier
.background(
color = if (isSelected) {
MaterialTheme.colorScheme.primary
JewelTheme.globalColors.text.disabledSelected
} else {
Color.Transparent
},

View File

@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
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.getValue
import androidx.compose.runtime.mutableStateOf
@@ -45,13 +43,13 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val focus by vm.focusIndex
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 ->
QuickStudent(idx, it, vm)
}
}
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if(focus == -1) {
Box(Modifier.weight(0.75f).fillMaxHeight()) {
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
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)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = JewelTheme.typography.h2TextStyle)
if(students[focus].contact.isNotBlank()) {
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))
}
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 {
var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) }
OutlinedTextField(mod, { mod = it })
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)
editing = false
})
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 {
FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
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) }) {
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))
Column(Modifier.weight(0.66f)) {
Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) {
Surface(shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn {
item {
Surface(tonalElevation = 15.dp) {
Surface {
Row(Modifier.padding(10.dp)) {
Text("Assignment", Modifier.weight(0.66f))
Text("Grade", Modifier.weight(0.33f))
@@ -194,7 +192,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@Composable
fun QuickStudent(idx: Int, student: Student, vm: EditionVM) {
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)) {
Text(student.name, fontWeight = FontWeight.Bold)
if(student.contact.isBlank())

View File

@@ -1,25 +1,32 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
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.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.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.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
@@ -32,10 +39,15 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.Grade
import kotlinx.datetime.*
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import java.util.*
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.component.*
import org.jetbrains.jewel.ui.theme.colorPalette
import org.jetbrains.jewel.ui.theme.iconData
import org.jetbrains.jewel.ui.typography
@Composable
@@ -80,7 +92,7 @@ fun ConfirmDeleteDialog(
onCloseRequest = onExit,
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
) {
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) {
Surface(Modifier.width(400.dp).height(300.dp)) {
Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) {
Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp))
@@ -153,8 +165,8 @@ fun Selectable(
) {
Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) selectedElevation else unselectedElevation,
shape = MaterialTheme.shapes.medium
markFocused = isSelected,
shape = JewelTheme.shapes.medium
) {
content()
}
@@ -197,7 +209,7 @@ fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (
Text("Used roles:")
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
items(used) {
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
Surface(Modifier.fillMaxWidth().clickable { role = it }) {
Text(it, Modifier.padding(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
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) }
@@ -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*/)
}
}
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
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -12,7 +13,13 @@ import androidx.compose.ui.backhandler.BackHandler
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
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 org.jetbrains.jewel.ui.component.*;
class Navigator private constructor(
private var _start: IDestination,
@@ -56,7 +63,7 @@ class Navigator private constructor(
inline fun <reified T : IDestination> backTo() = backTo(T::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DisplayScaffold() {
val state = remember { SnackbarHostState() }
@@ -73,21 +80,18 @@ class Navigator private constructor(
BackHandler { back() }
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
TitleBar(
modifier = Modifier.fillMaxWidth(),
title = { render.header(top.dest) },
navigationIcon = {
) {
IconButton({ back() }, enabled = top != _start) {
Icon(ChevronLeft, contentDescription = "Back")
Icon(Icons.ChevronLeft, contentDescription = "Back")
}
}
)
},
snackbarHost = {
SnackbarHost(state)
}
) { insets ->
Surface(Modifier.padding(insets), color = MaterialTheme.colorScheme.surface) {
snackState = state
) { //insets ->
Surface(/*Modifier.padding(insets),*/ color = JewelTheme.globalColors.panelBackground) {
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" }
directories = { group = "dev.dirs", name = "directories", version.ref = "directories" }
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]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }