Jewel-ize part I

This commit is contained in:
2026-03-26 15:02:00 +01:00
parent bdc56748dd
commit 18a7a82c36
19 changed files with 157 additions and 1580 deletions

View File

@@ -17,7 +17,9 @@ kotlin {
val desktopMain by getting
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
@@ -45,6 +47,7 @@ kotlin {
implementation(libs.directories)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.compose.backhandler)
implementation(libs.jewel)
}
}
}

View File

@@ -1,6 +1,5 @@
package com.jaytux.grader
import androidx.compose.material3.*
import androidx.compose.runtime.*
import com.jaytux.grader.data.v2.BaseAssignment
import com.jaytux.grader.data.v2.Course
@@ -16,6 +15,7 @@ import com.jaytux.grader.ui.PeerEvalsGradingView
import com.jaytux.grader.ui.SolosGradingTitle
import com.jaytux.grader.ui.SolosGradingView
import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
object Home : Navigator.IDestination
data class EditionDetail(val ed: Edition, val course: Course) : Navigator.IDestination
@@ -25,7 +25,7 @@ data class PeerEvalGrading(val course: Course, val edition: Edition, val assignm
@Composable
fun App() {
MaterialTheme {
IntUiTheme(isDark = true) {
Navigator.NavHost(Home) {
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }

View File

@@ -1,17 +1,12 @@
package com.jaytux.grader
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
import com.jaytux.grader.data.Database
import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.awt.Desktop
import java.net.URI
import java.time.Clock
import java.time.LocalDateTime
import java.util.prefs.Preferences
fun String.maxN(n: Int): String {

View File

@@ -2,7 +2,6 @@ package com.jaytux.grader
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.jaytux.grader.App
import com.jaytux.grader.data.Database
import io.github.vinceglb.filekit.FileKit

View File

@@ -1,715 +0,0 @@
package com.jaytux.grader.ui
//@Composable
//fun GroupAssignmentView(state: GroupAssignmentState) {
// val task by state.task
// val deadline by state.deadline
// val allFeedback by state.feedback.entities
// val criteria by state.criteria.entities
//
// var idx by remember(state) { mutableStateOf(0) }
//
// Column(Modifier.padding(10.dp)) {
// if(allFeedback.any { it.second.feedback == null }) {
// Text("Groups in bold have no feedback yet.", fontStyle = FontStyle.Italic)
// }
// else {
// Text("All groups have feedback.", fontStyle = FontStyle.Italic)
// }
//
// TabRow(idx) {
// Tab(idx == 0, { idx = 0 }) { Text("Task and Criteria") }
// allFeedback.forEachIndexed { i, it ->
// val (group, feedback) = it
// Tab(idx == i + 1, { idx = i + 1 }) {
// Text(group.name, fontWeight = feedback.feedback?.let { FontWeight.Normal } ?: FontWeight.Bold)
// }
// }
// }
//
// if(idx == 0) {
// groupTaskWidget(
// task, deadline, criteria,
// onSetTask = { state.updateTask(it) },
// onSetDeadline = { state.updateDeadline(it) },
// onAddCriterion = { state.addCriterion(it) },
// onModCriterion = { c, n, d -> state.updateCriterion(c, n, d) },
// onRmCriterion = { state.deleteCriterion(it) }
// )
// }
// else {
// groupFeedback(state, allFeedback[idx - 1].second)
// }
// }
//}
//
//@OptIn(ExperimentalMaterial3Api::class)
//@Composable
//fun groupTaskWidget(
// taskMD: String,
// deadline: LocalDateTime,
// criteria: List<GroupAssignmentCriterion>,
// onSetTask: (String) -> Unit,
// onSetDeadline: (LocalDateTime) -> Unit,
// onAddCriterion: (name: String) -> Unit,
// onModCriterion: (cr: GroupAssignmentCriterion, name: String, desc: String) -> Unit,
// onRmCriterion: (cr: GroupAssignmentCriterion) -> Unit
//) {
// var critIdx by remember { mutableStateOf(0) }
// var adding by remember { mutableStateOf(false) }
// var confirming by remember { mutableStateOf(false) }
//
// Row {
// Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
// Column(Modifier.padding(10.dp)) {
// LazyColumn(Modifier.weight(1f)) {
// item {
// Surface(
// Modifier.fillMaxWidth().clickable { critIdx = 0 },
// tonalElevation = if (critIdx == 0) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
// }
// }
//
// itemsIndexed(criteria) { i, crit ->
// Surface(
// Modifier.fillMaxWidth().clickable { critIdx = i + 1 },
// tonalElevation = if (critIdx == i + 1) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text(crit.name, Modifier.padding(5.dp))
// }
// }
// }
// Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
// Text("Add evaluation criterion")
// }
// }
// }
// Box(Modifier.weight(0.75f).padding(10.dp)) {
// if (critIdx == 0) {
// val updTask = rememberRichTextState()
//
// LaunchedEffect(taskMD) { updTask.setMarkdown(taskMD) }
//
// Column {
// Row {
// DateTimePicker(deadline, onSetDeadline)
// }
// RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") }
// CancelSaveRow(
// true,
// { updTask.setMarkdown(taskMD) },
// "Reset",
// "Update"
// ) { onSetTask(updTask.toMarkdown()) }
// }
// }
// else {
// val crit = criteria[critIdx - 1]
// var name by remember(crit) { mutableStateOf(crit.name) }
// var desc by remember(crit) { mutableStateOf(crit.description) }
//
// Column {
// Row {
// OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
// Spacer(Modifier.weight(0.1f))
// Button({ onModCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
// Text("Update")
// }
// }
// OutlinedTextField(
// desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
// label = { Text("Description") },
// singleLine = false,
// minLines = 5
// )
// Button({ confirming = true }, Modifier.fillMaxWidth()) {
// Text("Remove criterion")
// }
// }
// }
// }
// }
//
// if(adding) {
// AddStringDialog(
// "Evaluation criterion name", criteria.map{ it.name }, { adding = false }
// ) { onAddCriterion(it) }
// }
//
// if(confirming && critIdx != 0) {
// ConfirmDeleteDialog(
// "an evaluation criterion",
// { confirming = false }, { onRmCriterion(criteria[critIdx - 1]); critIdx = 0 }
// ) {
// Text(criteria[critIdx - 1].name)
// }
// }
//}
//
//@Composable
//fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
// val (group, feedback, individual) = fdbk
// var studentIdx by remember(fdbk) { mutableStateOf(0) }
// var critIdx by remember(fdbk) { mutableStateOf(0) }
// val criteria by state.criteria.entities
// val suggestions by state.autofill.entities
//
// val onSave = { grade: String, fdbk: String ->
// when {
// studentIdx == 0 && critIdx == 0 -> state.upsertGroupFeedback(group, fdbk, grade)
// studentIdx == 0 && critIdx != 0 -> state.upsertGroupFeedback(group, fdbk, grade, criteria[critIdx - 1])
// studentIdx != 0 && critIdx == 0 -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade)
// else -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade, criteria[critIdx - 1])
// }
// }
//
// val scope = rememberCoroutineScope()
// val exporter = rememberFileSaverLauncher { file ->
// file?.let {
// it.toKotlinxIoPath().parent?.let { path -> Preferences.set("exportPath", path.toString()) }
// scope.launch { fdbk.exportTo(it.toKotlinxIoPath(), state.assignment) }
// }
// }
//
// fun critGrade(crit: Int, student: Int = studentIdx): String? = when {
// student == 0 && crit == 0 -> feedback.global?.grade?.ifBlank { null }
// student == 0 && crit != 0 -> feedback.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
// student != 0 && crit == 0 -> individual[student - 1].second.second.global?.grade?.ifBlank { null }
// else -> individual[student - 1].second.second.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
// }
//
// Row {
// Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
// Column(Modifier.padding(10.dp)) {
// LazyColumn(Modifier.weight(1f)) {
// item {
// Surface(
// Modifier.fillMaxWidth().clickable { studentIdx = 0 },
// tonalElevation = if (studentIdx == 0) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
// }
// }
//
// itemsIndexed(individual.toList()) { i, (student, details) ->
// val (role, _) = details
// Surface(
// Modifier.fillMaxWidth().clickable { studentIdx = i + 1 },
// tonalElevation = if (studentIdx == i + 1) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
// }
// }
// }
//
// Button(
// { exporter.launch("${state.assignment.name} (${fdbk.group.name})", "md", Preferences.get("exportPath")?.let { PlatformFile(it) }) },
// Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
// ) {
// Text("Export group feedback")
// }
// }
// }
//
// Column(Modifier.weight(0.75f).padding(10.dp)) {
// TabRow(critIdx) {
// Tab(critIdx == 0, { critIdx = 0 }) {
// Text(
// "General feedback",
// Modifier.padding(5.dp),
// fontStyle = FontStyle.Italic
// )
// }
// criteria.forEachIndexed { i, c ->
// Tab(critIdx == i + 1, { critIdx = i + 1 }) {
// Text(
// c.name,
// Modifier.padding(5.dp)
// )
// }
// }
// }
//
// Spacer(Modifier.height(5.dp))
//
// groupFeedbackPane(
// criteria, critIdx, { critIdx = it },
// when {
// studentIdx == 0 && critIdx == 0 -> feedback.global
// studentIdx == 0 && critIdx != 0 -> feedback.byCriterion[critIdx - 1].entry
// studentIdx != 0 && critIdx == 0 -> individual[studentIdx - 1].second.second.global
// else -> individual[studentIdx - 1].second.second.byCriterion[critIdx - 1].entry
// },
// suggestions, onSave,
// if(critIdx == 0 && criteria.isNotEmpty()) criteria.mapIndexed { idx, it -> it.name to critGrade(idx + 1) } else null,
// if(studentIdx != 0 && critIdx == 0 && criteria.isNotEmpty()) {
// criteria.mapIndexed { idx, it -> critGrade(idx + 1, 0) }
// } else null,
// if(studentIdx != 0 && critIdx == 0) critGrade(0, 0) else null,
// key = studentIdx to critIdx
// )
// }
// }
//}
//
//@Composable
//fun groupFeedbackPane(
// criteria: List<GroupAssignmentCriterion>,
// currentCriterion: Int,
// onSelectCriterion: (Int) -> Unit,
// rawFeedback: GroupAssignmentState.FeedbackEntry?,
// autofill: List<String>,
// onSave: (String, String) -> Unit,
// critGrades: List<Pair<String, String?>>? = null,
// groupCritGrades: List<String?>? = null,
// groupOverallGrade: String? = null,
// modifier: Modifier = Modifier,
// key: Any? = null
//) {
// var grade by remember(rawFeedback, key) { mutableStateOf(rawFeedback?.grade ?: "") }
// val feedback = rememberRichTextState()
//
// LaunchedEffect(currentCriterion, criteria, rawFeedback, key) {
// feedback.setMarkdown(rawFeedback?.feedback ?: "")
// }
//
// Column(modifier) {
// Row {
// Text("Grade: ", Modifier.align(Alignment.CenterVertically))
// OutlinedTextField(
// grade, { grade = it }, Modifier.weight(0.45f),
// label = { Text(groupOverallGrade?.let { "Overall group grade: $it" } ?: "Grade") }
// )
// Spacer(Modifier.weight(0.35f))
// Button(
// { onSave(grade, feedback.toMarkdown()) },
// Modifier.weight(0.2f).align(Alignment.CenterVertically)
// ) {
// Text("Save")
// }
// }
// Spacer(Modifier.height(5.dp))
// Row {
// RichTextField(feedback, outerModifier = Modifier.weight(0.7f).fillMaxHeight()) { Text("Feedback") }
// critGrades?.let { grades ->
// Spacer(Modifier.width(10.dp))
// LazyColumn(Modifier.weight(0.3f)) {
// item {
// Text("Criteria grades", Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
// }
// itemsIndexed(grades) { idx, (crit, grade) ->
// Column {
// Text(crit, Modifier.padding(5.dp), fontWeight = FontWeight.Bold)
// Row {
// Spacer(Modifier.width(5.dp))
//
// val (text, style) = if(grade == null || grade.isBlank()) {
// if(groupCritGrades == null || groupCritGrades[idx]?.isBlank() != false) {
// "(no grade yet)"
// } else {
// "Group grade: ${groupCritGrades[idx]}"
// } to FontStyle.Italic
// } else {
// if(groupCritGrades == null || groupCritGrades[idx]?.isBlank() != false) {
// grade to FontStyle.Normal
// }
// else {
// "$grade (group: ${groupCritGrades[idx]})" to FontStyle.Normal
// }
// }
//
// Text(text, Modifier.padding(5.dp), fontStyle = style)
// }
// Spacer(Modifier.width(10.dp))
// }
// }
// }
// }
// }
// }
//}
//
//@OptIn(ExperimentalMaterial3Api::class)
//@Composable
//fun SoloAssignmentView(state: SoloAssignmentState) {
// val task by state.task
// val deadline by state.deadline
// val suggestions by state.autofill.entities
// val grades by state.feedback.entities
// val criteria by state.criteria.entities
//
// var tab by remember(state) { mutableStateOf(0) }
// var idx by remember(state, tab) { mutableStateOf(0) }
// var critIdx by remember(state, tab, idx) { mutableStateOf(0) }
// var adding by remember(state, tab) { mutableStateOf(false) }
// var confirming by remember(state, tab) { mutableStateOf(false) }
//
// val updateGrade = { grade: String ->
// state.upsertFeedback(
// grades[idx].first,
// if(critIdx == 0) grades[idx].second.global?.feedback else grades[idx].second.byCriterion[critIdx - 1].second?.feedback,
// grade,
// if(critIdx == 0) null else criteria[critIdx - 1]
// )
// }
//
// val updateFeedback = { feedback: String ->
// state.upsertFeedback(
// grades[idx].first,
// feedback,
// if(critIdx == 0) grades[idx].second.global?.grade else grades[idx].second.byCriterion[critIdx - 1].second?.grade,
// if(critIdx == 0) null else criteria[critIdx - 1]
// )
// }
//
// Column(Modifier.padding(10.dp)) {
// Row {
// Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
// Column(Modifier.padding(10.dp)) {
// TabRow(tab) {
// Tab(tab == 0, { tab = 0 }) { Text("Task/Criteria") }
// Tab(tab == 1, { tab = 1 }) { Text("Students") }
// }
//
// LazyColumn(Modifier.weight(1f)) {
// if (tab == 0) {
// item {
// Surface(
// Modifier.fillMaxWidth().clickable { idx = 0 },
// tonalElevation = if (idx == 0) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
// }
// }
//
// itemsIndexed(criteria) { i, crit ->
// Surface(
// Modifier.fillMaxWidth().clickable { idx = i + 1 },
// tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text(crit.name, Modifier.padding(5.dp))
// }
// }
// } else {
// itemsIndexed(grades.toList()) { i, (student, _) ->
// Surface(
// Modifier.fillMaxWidth().clickable { idx = i },
// tonalElevation = if (idx == i) 50.dp else 0.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Text(student.name, Modifier.padding(5.dp))
// }
// }
// }
// }
//
// if (tab == 0) {
// Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
// Text("Add evaluation criterion")
// }
// }
// }
// }
//
// Column(Modifier.weight(0.75f).padding(10.dp)) {
// if(tab == 0) {
// if (idx == 0) {
// val updTask = rememberRichTextState()
//
// LaunchedEffect(task) { updTask.setMarkdown(task) }
//
// Row {
// DateTimePicker(deadline, { state.updateDeadline(it) })
// }
// RichTextStyleRow(state = updTask)
// OutlinedRichTextEditor(
// state = updTask,
// modifier = Modifier.fillMaxWidth().weight(1f),
// singleLine = false,
// minLines = 5,
// label = { Text("Task") }
// )
// CancelSaveRow(
// true,
// { updTask.setMarkdown(task) },
// "Reset",
// "Update"
// ) { state.updateTask(updTask.toMarkdown()) }
// } else {
// val crit = criteria[idx - 1]
// var name by remember(crit) { mutableStateOf(crit.name) }
// var desc by remember(crit) { mutableStateOf(crit.description) }
//
// Column {
// Row {
// OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
// Spacer(Modifier.weight(0.1f))
// Button({ state.updateCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
// Text("Update")
// }
// }
// OutlinedTextField(
// desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
// label = { Text("Description") },
// singleLine = false,
// minLines = 5
// )
// Button({ confirming = true }, Modifier.fillMaxWidth()) {
// Text("Remove criterion")
// }
// }
// }
// }
// else {
// soloFeedbackPane(
// criteria, critIdx, { critIdx = it }, grades[idx].second.global,
// if(critIdx == 0) grades[idx].second.global else grades[idx].second.byCriterion[critIdx - 1].second,
// suggestions, updateGrade, updateFeedback,
// key = tab to idx
// )
// }
// }
// }
// }
//
// if(adding) {
// AddStringDialog(
// "Evaluation criterion name", criteria.map{ it.name }, { adding = false }
// ) { state.addCriterion(it) }
// }
//
// if(confirming && idx != 0) {
// ConfirmDeleteDialog(
// "an evaluation criterion",
// { confirming = false }, { state.deleteCriterion(criteria[idx - 1]); idx = 0 }
// ) {
// Text(criteria[idx - 1].name)
// }
// }
//}
//
//@Composable
//fun soloFeedbackPane(
// criteria: List<SoloAssignmentCriterion>,
// currentCriterion: Int,
// onSelectCriterion: (Int) -> Unit,
// globFeedback: SoloAssignmentState.LocalFeedback?,
// criterionFeedback: SoloAssignmentState.LocalFeedback?,
// autofill: List<String>,
// onSetGrade: (String) -> Unit,
// onSetFeedback: (String) -> Unit,
// modifier: Modifier = Modifier,
// key: Any? = null
//) {
// var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
// val feedback = rememberRichTextState()
//
// LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) {
// feedback.setMarkdown(criterionFeedback?.feedback ?: "")
// }
// Column(modifier) {
// Row {
// Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
// OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
// Spacer(Modifier.weight(0.6f))
// Button(
// { onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) },
// Modifier.weight(0.2f).align(Alignment.CenterVertically)
// ) {
// Text("Save")
// }
// }
// TabRow(currentCriterion) {
// Tab(currentCriterion == 0, { onSelectCriterion(0) }) { Text("General feedback", fontStyle = FontStyle.Italic) }
// criteria.forEachIndexed { i, c ->
// Tab(currentCriterion == i + 1, { onSelectCriterion(i + 1) }) { Text(c.name) }
// }
// }
// Spacer(Modifier.height(5.dp))
// RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
// }
//}
//
//@Composable
//fun PeerEvaluationView(state: PeerEvaluationState) {
// val contents by state.contents.entities
// var idx by remember(state) { mutableStateOf(0) }
// var editing by remember(state) { mutableStateOf<Triple<Student, Student?, PeerEvaluationState.Student2StudentEntry?>?>(null) }
// val measure = rememberTextMeasurer()
//
// val isSelected = { from: Student, to: Student? ->
// editing?.let { (f, t, _) -> f == from && t == to } ?: false
// }
//
// Column(Modifier.padding(10.dp)) {
// TabRow(idx) {
// contents.forEachIndexed { i, it ->
// Tab(idx == i, { idx = i; editing = null }) { Text(it.group.name) }
// }
// }
// Spacer(Modifier.height(10.dp))
//
// Row {
// val current = contents[idx]
// val horScroll = rememberLazyListState()
// val style = LocalTextStyle.current
// val textLenMeasured = remember(state, idx) {
// current.students.maxOf { (s, _) ->
// measure.measure(s.name, style).size.width
// } + 10
// }
// val cellSize = 75.dp
//
// Column(Modifier.weight(0.5f)) {
// Row {
// Box { FromTo(textLenMeasured.dp) }
// LazyRow(Modifier.height(textLenMeasured.dp), state = horScroll) {
// item { VLine() }
// items(current.students) { (s, _) ->
// Box(
// Modifier.width(cellSize).height(textLenMeasured.dp),
// contentAlignment = Alignment.TopCenter
// ) {
// var _h: Int = 0
// Text(s.name, Modifier.layout{ m, c ->
// val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
// _h = p.height
// layout(p.height, p.width) { p.place(0, 0) }
// }.graphicsLayer {
// rotationZ = -90f
// transformOrigin = TransformOrigin(0f, 0.5f)
// translationX = _h.toFloat() / 2f
// translationY = textLenMeasured.dp.value - 15f
// })
// }
// }
// item { VLine() }
// item {
// Box(
// Modifier.width(cellSize).height(textLenMeasured.dp),
// contentAlignment = Alignment.TopCenter
// ) {
// var _h: Int = 0
// Text("Group Rating", Modifier.layout{ m, c ->
// val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
// _h = p.height
// layout(p.height, p.width) { p.place(0, 0) }
// }.graphicsLayer {
// rotationZ = -90f
// transformOrigin = TransformOrigin(0f, 0.5f)
// translationX = _h.toFloat() / 2f
// translationY = textLenMeasured.dp.value - 15f
// }, fontWeight = FontWeight.Bold)
// }
// }
// item { VLine() }
// }
// }
// MeasuredLazyColumn(key = idx) {
// measuredItem { HLine() }
// items(current.students) { (from, role, glob, map) ->
// Row(Modifier.height(cellSize)) {
// Column(Modifier.width(textLenMeasured.dp).align(Alignment.CenterVertically)) {
// Text(from.name, Modifier.width(textLenMeasured.dp))
// role?.let { r ->
// Row {
// Spacer(Modifier.width(10.dp))
// Text(
// r,
// style = MaterialTheme.typography.bodySmall,
// fontStyle = FontStyle.Italic
// )
// }
// }
// }
// LazyRow(state = horScroll) {
// item { VLine() }
// items(map) { (to, entry) ->
// PEGradeWidget(entry,
// { editing = Triple(from, to, entry) }, { editing = null },
// isSelected(from, to), Modifier.size(cellSize, cellSize)
// )
// }
// item { VLine() }
// item {
// PEGradeWidget(glob,
// { editing = Triple(from, null, glob) }, { editing = null },
// isSelected(from, null), Modifier.size(cellSize, cellSize))
// }
// item { VLine() }
// }
// }
// }
// measuredItem { HLine() }
// }
// }
//
// Column(Modifier.weight(0.5f)) {
// var groupLevel by remember(state, idx) { mutableStateOf(contents[idx].content) }
// editing?.let {
// Column(Modifier.weight(0.5f)) {
// val (from, to, data) = it
//
// var sGrade by remember(editing) { mutableStateOf(data?.grade ?: "") }
// var sMsg by remember(editing) { mutableStateOf(data?.feedback ?: "") }
//
// Box(Modifier.padding(5.dp)) {
// to?.let { s2 ->
// if(from == s2)
// Text("Self-evaluation by ${from.name}", fontWeight = FontWeight.Bold)
// else
// Text("Evaluation of ${s2.name} by ${from.name}", fontWeight = FontWeight.Bold)
// } ?: Text("Group-level evaluation by ${from.name}", fontWeight = FontWeight.Bold)
// }
//
// Row {
// Text("Grade: ", Modifier.align(Alignment.CenterVertically))
// OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
// Spacer(Modifier.weight(0.6f))
// Button(
// { state.upsertIndividualFeedback(from, to, sGrade, sMsg); editing = null },
// Modifier.weight(0.2f).align(Alignment.CenterVertically),
// enabled = sGrade.isNotBlank() || sMsg.isNotBlank()
// ) {
// Text("Save")
// }
// }
//
// OutlinedTextField(
// sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f),
// label = { Text("Feedback") },
// singleLine = false,
// minLines = 5
// )
// }
// }
//
// Column(Modifier.weight(0.5f)) {
// Row {
// Text("Group-level notes", Modifier.weight(1f).align(Alignment.CenterVertically), fontWeight = FontWeight.Bold)
// Button(
// { state.upsertGroupFeedback(current.group, groupLevel); editing = null },
// enabled = groupLevel != contents[idx].content
// ) { Text("Update") }
// }
//
// OutlinedTextField(
// groupLevel, { groupLevel = it }, Modifier.fillMaxWidth().weight(1f),
// label = { Text("Group-level notes") },
// singleLine = false,
// minLines = 5
// )
// }
// }
// }
// }
//}

View File

@@ -4,7 +4,17 @@ 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.*
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
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -29,6 +39,9 @@ import kotlinx.datetime.*
import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.char
import kotlin.time.Instant
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fillMaxSize()) {
@@ -73,7 +86,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
val peerEvalData by vm.asPeerEvaluation.entity
var updatingPeerEvalGrade by remember { mutableStateOf(false) }
Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium)
Text(assignment.assignment.name, style = JewelTheme.typography.h2TextStyle)
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))
@@ -115,7 +128,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Row {
Column(Modifier.weight(0.75f)) {
Row {
Text("Description:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp).weight(1f))
Text("Description:", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(top = 10.dp).weight(1f))
Button({ vm.setDesc(assignment, descRtf.toMarkdown()) }) {
Text("Update")
}
@@ -127,7 +140,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Surface(Modifier.weight(0.25f), color = Color.White) {
Column(Modifier.padding(15.dp)) {
Row {
Text("Grading Rubrics", Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ addingRubric = true }) {
Icon(CirclePlus, "Add grading rubric")
}
@@ -328,7 +341,7 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
Surface(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) {
Text("Select a grading scale for $name", style = MaterialTheme.typography.headlineSmall, 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)) {
Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
@@ -470,7 +483,7 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Grading system name") }, isError = name in taken, singleLine = true)
Text("Grade options:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp))
Text("Grade options:", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(top = 10.dp))
LazyColumn(Modifier.weight(1f)) {
itemsIndexed(options) { idx, it ->
Row(Modifier.fillMaxWidth().padding(5.dp)) {

View File

@@ -1,87 +0,0 @@
package com.jaytux.grader.ui
//@Composable
//fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
// val data by state.courses.entities
// var showDialog by remember { mutableStateOf(false) }
//
// Box(Modifier.padding(15.dp)) {
// ListOrEmpty(
// data,
// { Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
// { Text("Add a course") },
// { showDialog = true },
// addAfterLazy = false
// ) { _, it ->
// CourseWidget(state.getEditions(it), { state.delete(it) }, push)
// }
// }
//
// if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
//}
//
//@Composable
//fun CourseWidget(state: EditionListState, onDelete: () -> Unit, push: (UiRoute) -> Unit) {
// val editions by state.editions.entities
// var isOpened by remember { mutableStateOf(false) }
// var showDialog by remember { mutableStateOf(false) }
//
// val callback = { it: Edition ->
// val s = EditionState(it)
// val route = UiRoute("${state.course.name}: ${it.name}") {
// EditionView(s)
// }
// push(route)
// }
//
// Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { isOpened = !isOpened }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
// Row {
// Column(Modifier.weight(1f).padding(5.dp)) {
// Row {
// Icon(
// if (isOpened) ChevronDown else ChevronRight, "Toggle editions",
// Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp())
// .align(Alignment.CenterVertically)
// )
// Column {
// Text(state.course.name, style = MaterialTheme.typography.headlineMedium)
// }
// }
// Row {
// Spacer(Modifier.width(25.dp))
// Text(
// "${editions.size} edition(s)",
// fontStyle = FontStyle.Italic,
// style = MaterialTheme.typography.bodySmall
// )
// }
//
// if(isOpened) {
// Row {
// Spacer(Modifier.width(25.dp))
// Column {
// editions.forEach { EditionWidget(it, { callback(it) }) { state.delete(it) } }
// Button({ showDialog = true }, Modifier.fillMaxWidth()) { Text("Add edition") }
// }
// }
// }
// }
// Column {
// IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
// IconButton({ TODO() }, enabled = false) { Icon(Icons.Default.Edit, "Edit") }
// }
// }
// }
//
// if(showDialog) AddStringDialog("Edition name", editions.map { it.name }, { showDialog = false }) { state.new(it) }
//}
//
//@Composable
//fun EditionWidget(edition: Edition, onOpen: () -> Unit, onDelete: () -> Unit) {
// Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { onOpen() }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
// Row(Modifier.padding(5.dp)) {
// Text(edition.name, Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
// IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
// }
// }
//}

View File

@@ -1,33 +1,18 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.EditionDetail
import com.jaytux.grader.data.v2.BaseAssignment
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun EditionTitle(data: EditionDetail) = Text("Courses / ${data.course.name} / ${data.ed.name}")
@@ -44,8 +29,8 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
Column(Modifier.padding(10.dp)) {
Row {
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = MaterialTheme.typography.headlineMedium)
Button({ adding = true }) {
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ adding = true }) {
Icon(CirclePlus, "Add ${tab.addText}")
Spacer(Modifier.width(5.dp))
Text("Add ${tab.addText}")

View File

@@ -1,409 +0,0 @@
package com.jaytux.grader.ui
//data class Navigators(
// val student: (Student) -> Unit,
// val group: (Group) -> Unit,
// val assignment: (Assignment) -> Unit
//)
//
//@Composable
//fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
// val course = state.course; val edition = state.edition
// val students by state.students.entities
// val availableStudents by state.availableStudents.entities
// val groups by state.groups.entities
// val solo by state.solo.entities
// val groupAs by state.groupAs.entities
// val peers by state.peer.entities
// val mergedAssignments by remember(solo, groupAs, peers) { mutableStateOf(Assignment.merge(groupAs, solo, peers)) }
// val hist by state.history
//
// val scope = rememberCoroutineScope()
//
// var groupExporting by remember { mutableStateOf<GroupAssignmentState?>(null) }
// val groupPopup = rememberDirectoryPickerLauncher(directory = PlatformFile(Preferences.exportPath)) { path ->
// if(path != null) {
// groupExporting?.let {
// Preferences.exportPath = path.toKotlinxIoPath().toString()
// scope.launch { it.batchExport(path.toKotlinxIoPath()) }
// }
// }
// }
//
// val navs = Navigators(
// student = { state.navTo(OpenPanel.Student, students.indexOfFirst{ s -> s.id == it.id }) },
// group = { state.navTo(OpenPanel.Group, groups.indexOfFirst { g -> g.id == it.id }) },
// assignment = { state.navTo(OpenPanel.Assignment, mergedAssignments.indexOfFirst { a -> a.id() == it.id() }) }
// )
//
// val (id, tab) = hist.last()
// Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
// TabLayout(
// OpenPanel.entries,
// tab.ordinal,
// { state.navTo(OpenPanel.entries[it]) },
// { Text(it.tabName) }
// ) {
// when(tab) {
// OpenPanel.Student -> StudentPanel(
// course, edition, students, availableStudents, id,
// { state.navTo(it) },
// { name, note, contact, add -> state.newStudent(name, contact, note, add) },
// { students -> state.addToCourse(students) },
// { s, name -> state.setStudentName(s, name) }
// ) { s, idx -> state.delete(s); if(id == idx) state.clearHistoryIndex() }
//
// OpenPanel.Group -> GroupPanel(
// course, edition, groups, id,
// { state.navTo(it) },
// { name -> state.newGroup(name) },
// { g, name -> state.setGroupName(g, name) }
// ) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
//
// OpenPanel.Assignment -> AssignmentPanel(
// course, edition, mergedAssignments, id,
// { state.navTo(it) },
// { type, name -> state.newAssignment(type, name) },
// { a, name -> state.setAssignmentTitle(a, name) },
// { a1, a2 -> state.swapOrder(a1, a2) }
// ) { a, idx -> state.delete(a); if(id == idx) state.clearHistoryIndex() }
// }
// }
// }
//
// Column(Modifier.weight(0.75f)) {
// Row {
// IconButton({ state.back() }, enabled = hist.size >= 2) {
// Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp()).align(Alignment.CenterVertically))
// }
// when(tab) {
// OpenPanel.Student -> {
// if(id == -1) PaneHeader("Nothing selected", "students", course, edition)
// else PaneHeader(students[id].name, "student", course, edition)
// }
// OpenPanel.Group -> {
// if(id == -1) PaneHeader("Nothing selected", "groups", course, edition)
// else PaneHeader(groups[id].name, "group", course, edition)
// }
// OpenPanel.Assignment -> {
// if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition)
// else {
// when(val a = mergedAssignments[id]) {
// is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
// is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition) {
// groupExporting = GroupAssignmentState(a.assignment); groupPopup.launch()
// }
// is Assignment.PeerEval -> PaneHeader(a.name(), "peer evaluation", course, edition)
// }
// }
// }
// }
// }
// Box(Modifier.weight(1f)) {
// if (id != -1) {
// when (tab) {
// OpenPanel.Student -> StudentView(StudentState(students[id], edition), navs)
// OpenPanel.Group -> GroupView(GroupState(groups[id]), navs)
// OpenPanel.Assignment -> {
// when (val a = mergedAssignments[id]) {
// is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
// is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
// is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation))
// }
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun StudentPanel(
// course: Course, edition: Edition, students: List<Student>, available: List<Student>,
// selected: Int, onSelect: (Int) -> Unit,
// onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
// onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student, Int) -> Unit
//) = Column(Modifier.padding(10.dp)) {
// var showDialog by remember { mutableStateOf(false) }
// var deleting by remember { mutableStateOf(-1) }
// var editing by remember { mutableStateOf(-1) }
//
// Text("Student list (${students.size})", style = MaterialTheme.typography.headlineMedium)
//
// ListOrEmpty(
// students,
// { Text(
// "Course ${course.name} (edition ${edition.name})\nhas no students yet.",
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
// ) },
// { Text("Add a student") },
// { showDialog = true }
// ) { idx, it ->
// SelectEditDeleteRow(
// selected == idx,
// { onSelect(idx) }, { onSelect(-1) },
// { editing = idx }, { deleting = idx }
// ) {
// Text(it.name, Modifier.padding(5.dp))
// }
// }
//
// if(showDialog) {
// StudentDialog(course, edition, { showDialog = false }, available, onImport, onAdd)
// }
// else if(editing != -1) {
// AddStringDialog("Student name", students.map { it.name }, { editing = -1 }, students[editing].name) {
// onUpdate(students[editing], it)
// }
// }
// else if(deleting != -1) {
// ConfirmDeleteDialog(
// "a student",
// { deleting = -1 },
// { onDelete(students[deleting], deleting) }
// ) { Text(students[deleting].name) }
// }
//}
//
//@Composable
//fun GroupPanel(
// course: Course, edition: Edition, groups: List<Group>,
// selected: Int, onSelect: (Int) -> Unit,
// onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group, Int) -> Unit
//) = Column(Modifier.padding(10.dp)) {
// var showDialog by remember { mutableStateOf(false) }
// var deleting by remember { mutableStateOf(-1) }
// var editing by remember { mutableStateOf(-1) }
//
// Text("Group list (${groups.size})", style = MaterialTheme.typography.headlineMedium)
//
// ListOrEmpty(
// groups,
// { Text(
// "Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
// ) },
// { Text("Add a group") },
// { showDialog = true }
// ) { idx, it ->
// SelectEditDeleteRow(
// selected == idx,
// { onSelect(idx) }, { onSelect(-1) },
// { editing = idx }, { deleting = idx }
// ) {
// Text(it.name, Modifier.padding(5.dp))
// }
// }
//
// if(showDialog) {
// AddStringDialog("Group name", groups.map{ it.name }, { showDialog = false }) { onAdd(it) }
// }
// else if(editing != -1) {
// AddStringDialog("Group name", groups.map { it.name }, { editing = -1 }, groups[editing].name) {
// onUpdate(groups[editing], it)
// }
// }
// else if(deleting != -1) {
// ConfirmDeleteDialog(
// "a group",
// { deleting = -1 },
// { onDelete(groups[deleting], deleting) }
// ) { Text(groups[deleting].name) }
// }
//}
//
//@Composable
//fun AssignmentPanel(
// course: Course, edition: Edition, assignments: List<Assignment>,
// selected: Int, onSelect: (Int) -> Unit,
// onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
// onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment, Int) -> Unit
//) = Column(Modifier.padding(10.dp)) {
// var showDialog by remember { mutableStateOf(false) }
// var deleting by remember { mutableStateOf(-1) }
// var editing by remember { mutableStateOf(-1) }
//
// val dialog: @Composable (String, List<String>, () -> Unit, String, (AssignmentType, String) -> Unit) -> Unit =
// { label, taken, onClose, current, onSave ->
// DialogWindow(
// onCloseRequest = onClose,
// state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
// ) {
// var name by remember(current) { mutableStateOf(current) }
// var tab by remember { mutableStateOf(AssignmentType.Solo) }
//
// Surface(Modifier.fillMaxSize()) {
// TabLayout(
// AssignmentType.entries,
// tab.ordinal,
// { tab = AssignmentType.entries[it] },
// { Text(it.show) }
// ) {
// Box(Modifier.fillMaxSize().padding(10.dp)) {
// Column(Modifier.align(Alignment.Center)) {
// OutlinedTextField(
// name,
// { name = it },
// Modifier.fillMaxWidth(),
// label = { Text(label) },
// isError = name in taken
// )
// CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
// onSave(tab, name)
// onClose()
// }
// }
// }
// }
// }
// }
// }
//
// Text("Assignment list (${assignments.size})", style = MaterialTheme.typography.headlineMedium)
//
// ListOrEmpty(
// assignments,
// { Text(
// "Course ${course.name} (edition ${edition.name})\nhas no assignments yet.",
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
// ) },
// { Text("Add an assignment") },
// { showDialog = true }
// ) { idx, it ->
// Selectable(
// selected == idx,
// { onSelect(idx) }, { onSelect(-1) }
// ) {
// Row {
// Text(it.name(), Modifier.padding(5.dp).align(Alignment.CenterVertically).weight(1f))
// Column(Modifier.padding(2.dp)) {
// Icon(Icons.Default.ArrowUpward, "Move up", Modifier.clickable {
// if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1])
// })
// Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable {
// if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1])
// })
// }
// Column(Modifier.padding(2.dp)) {
// Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx })
// Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = idx })
// }
// }
// }
// }
//
// if(showDialog) {
// dialog("Assignment name", assignments.map{ it.name() }, { showDialog = false }, "", onAdd)
// }
// else if(editing != -1) {
// AddStringDialog("Assignment name", assignments.map { it.name() }, { editing = -1 }, assignments[editing].name()) {
// onUpdate(assignments[editing], it)
// }
// }
// else if(deleting != -1) {
// ConfirmDeleteDialog(
// "an assignment",
// { deleting = -1 },
// { onDelete(assignments[deleting], deleting) }
// ) { if(deleting != -1) Text(assignments[deleting].name()) }
// }
//}
//
//@Composable
//fun StudentDialog(
// course: Course,
// edition: Edition,
// onClose: () -> Unit,
// availableStudents: List<Student>,
// onImport: (List<Student>) -> Unit,
// onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
//) = DialogWindow(
// onCloseRequest = onClose,
// state = rememberDialogState(size = DpSize(600.dp, 400.dp), position = WindowPosition(Alignment.Center))
//) {
// Surface(Modifier.fillMaxSize()) {
// Column(Modifier.padding(10.dp)) {
// var isImport by remember { mutableStateOf(false) }
// TabRow(if(isImport) 1 else 0) {
// Tab(!isImport, { isImport = false }) { Text("Add new student") }
// Tab(isImport, { isImport = true }) { Text("Add existing student") }
// }
//
// if(isImport) {
// if(availableStudents.isEmpty()) {
// Box(Modifier.fillMaxSize()) {
// Text("No students available to add to this course.", Modifier.align(Alignment.Center))
// }
// }
// else {
// var selected by remember { mutableStateOf(setOf<Int>()) }
//
// val onClick = { idx: Int ->
// selected = if(idx in selected) selected - idx else selected + idx
// }
//
// Text("Select students to add to ${course.name} ${edition.name}")
// LazyColumn {
// itemsIndexed(availableStudents) { idx, student ->
// Surface(
// Modifier.fillMaxWidth().clickable { onClick(idx) },
// tonalElevation = if (selected.contains(idx)) 5.dp else 0.dp
// ) {
// Row {
// Checkbox(selected.contains(idx), { onClick(idx) })
// Text(student.name, Modifier.padding(5.dp))
// }
// }
// }
// }
// CancelSaveRow(selected.isNotEmpty(), onClose) {
// onImport(selected.map { idx -> availableStudents[idx] })
// onClose()
// }
// }
// }
// else {
// Box(Modifier.fillMaxSize()) {
// var name by remember { mutableStateOf("") }
// var contact by remember { mutableStateOf("") }
// var note by remember { mutableStateOf("") }
// var add by remember { mutableStateOf(true) }
//
// Column(Modifier.align(Alignment.Center)) {
// OutlinedTextField(
// name,
// { name = it },
// Modifier.fillMaxWidth(),
// singleLine = true,
// label = { Text("Student name") })
// OutlinedTextField(
// contact,
// { contact = it },
// Modifier.fillMaxWidth(),
// singleLine = true,
// label = { Text("Student contact") })
// OutlinedTextField(
// note,
// { note = it },
// Modifier.fillMaxWidth(),
// singleLine = false,
// minLines = 3,
// label = { Text("Note") })
// Row {
// Checkbox(add, { add = it })
// Text(
// "Add student to ${course.name} ${edition.name}?",
// Modifier.align(Alignment.CenterVertically)
// )
// }
// CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
// onAdd(name, note, contact, add)
// onClose()
// }
// }
// }
// }
// }
// }
//}

View File

@@ -1,36 +1,13 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.clickable
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.ProvideTextStyle
//import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -40,16 +17,13 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading
import com.jaytux.grader.app
import com.jaytux.grader.data.v2.CategoricGrade
import com.jaytux.grader.data.v2.Criterion
import com.jaytux.grader.data.v2.GradeType
import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.viewmodel.Grade
import com.jaytux.grader.viewmodel.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.util.UUID
import java.util.*
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun GroupsGradingTitle(data: GroupGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Group Assignments / ${data.assignment.name} / Grading")
@@ -65,7 +39,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) }
Column(Modifier.padding(10.dp)) {
Text("Grading ${vm.base.name}", style = MaterialTheme.typography.headlineMedium)
Text("Grading ${vm.base.name}", style = JewelTheme.typography.h2TextStyle)
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) {
@@ -87,7 +61,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
Icon(DoubleBack, "Previous group")
}
Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall)
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")
@@ -154,15 +128,15 @@ fun GFWidget(
Spacer(Modifier.width(5.dp))
Column(Modifier.align(Alignment.CenterVertically)) {
Row {
Text(overrideName ?: crit.criterion.name, style = MaterialTheme.typography.bodyLarge)
Text(overrideName ?: crit.criterion.name, style = JewelTheme.typography.h4TextStyle)
Spacer(Modifier.width(5.dp))
feedback.groupLevel?.grade?.let {
Row(Modifier.align(Alignment.Bottom)) {
ProvideTextStyle(MaterialTheme.typography.bodySmall) {
// ProvideTextStyle(JewelTheme.typography.small) {
Text("(Grade: ")
it.render()
Text(")")
}
// }
}
}
}
@@ -183,7 +157,7 @@ fun GFWidget(
Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp))
Button({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
DefaultButton({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save grade and feedback")
}
}
@@ -193,7 +167,7 @@ fun GFWidget(
Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), tonalElevation = 10.dp, shape = MaterialTheme.shapes.small) {
Column(Modifier.padding(10.dp)) {
Text("Individual overrides", style = MaterialTheme.typography.bodyLarge)
Text("Individual overrides", style = JewelTheme.typography.h4TextStyle)
feedback.overrides.forEach { (student, it) ->
var enable by remember(key, it) { mutableStateOf(it != null) }
var maybeRemoving by remember(key, it) { mutableStateOf(false) }
@@ -207,7 +181,7 @@ fun GFWidget(
Text(student.name, Modifier.align(Alignment.CenterVertically))
if(student.id.value in markOverridden) {
Spacer(Modifier.width(5.dp))
Text("(Overridden)", Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.bodySmall, fontStyle = FontStyle.Italic, color = Color.Red)
Text("(Overridden)", Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.small, fontStyle = FontStyle.Italic, color = Color.Red)
}
}
@@ -220,7 +194,7 @@ fun GFWidget(
Spacer(Modifier.height(5.dp))
OutlinedTextField(sText, { sText = it }, label = { Text("Feedback") }, singleLine = true, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(5.dp))
Button({ vm.modOverrideFeedback(crit.criterion, gr, student, groupLevel, sGrade, sText) }) {
DefaultButton({ vm.modOverrideFeedback(crit.criterion, gr, student, groupLevel, sGrade, sText) }) {
Text("Save override")
}
}

View File

@@ -19,12 +19,8 @@ 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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -55,6 +51,10 @@ import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable
import java.util.UUID
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalTextStyle
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@@ -81,7 +81,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
else {
Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(group.group.name, style = MaterialTheme.typography.headlineMedium)
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())
@@ -106,7 +106,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
item {
Surface(tonalElevation = 15.dp) {
Row(Modifier.fillMaxWidth().padding(10.dp)) {
Text("Members", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp))
Text("Members", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp))
}
}
}
@@ -122,7 +122,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
if(role != null) {
Surface(Modifier.align(Alignment.CenterVertically), tonalElevation = 5.dp, shape = MaterialTheme.shapes.small) {
Box(Modifier.clickable { swappingRole = -1 }.clickable { swappingRole = idx }) {
Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = MaterialTheme.typography.labelMedium)
Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = JewelTheme.typography.regular)
}
}
}
@@ -148,7 +148,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Spacer(Modifier.height(10.dp))
Column(Modifier.weight(0.5f)) {
Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall)
Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) {
LazyColumn(Modifier.fillMaxHeight()) {
item {
@@ -190,7 +190,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
item {
Surface(tonalElevation = 15.dp) {
Row(Modifier.fillMaxWidth().padding(10.dp)) {
Text("Available Students", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp))
Text("Available Students", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp))
}
}
}

View File

@@ -4,7 +4,8 @@ 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.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -14,6 +15,9 @@ import com.jaytux.grader.EditionDetail
import com.jaytux.grader.data.v2.Edition
import com.jaytux.grader.viewmodel.HomeVM
import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun HomeTitle() = Text("Grader")
@@ -27,8 +31,8 @@ fun HomeView(token: Navigator.NavToken) {
LazyColumn(Modifier.padding(15.dp)) {
item {
Row {
Text("Courses Overview", Modifier.weight(0.8f), style = MaterialTheme.typography.headlineMedium)
Button({ addingCourse = true }) {
Text("Courses Overview", Modifier.weight(0.8f), style = JewelTheme.typography.h2TextStyle)
DefaultButton({ addingCourse = true }) {
Icon(CirclePlus, "Add course")
Spacer(Modifier.width(5.dp))
Text("Add course")
@@ -48,7 +52,6 @@ fun HomeView(token: Navigator.NavToken) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) {
var addingEdition by remember { mutableStateOf(false) }
@@ -56,13 +59,13 @@ fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.fillMaxWidth().padding(10.dp)) {
Column(Modifier.padding(8.dp)) {
Row {
Text(course.course.name, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f))
Text(course.course.name, style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f))
IconButton({ deleting = true }) { Icon(Delete, "Delete course") }
}
Row {
Text("Editions", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f))
Button({ addingEdition = true }) {
Text("Editions", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f))
DefaultButton({ addingEdition = true }) {
Icon(CirclePlus, "Add edition")
Spacer(Modifier.width(5.dp))
Text("Add edition")
@@ -74,7 +77,7 @@ fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -
}
if(course.archived.isNotEmpty()) {
Text("Archived editions", style = MaterialTheme.typography.headlineSmall)
Text("Archived editions", style = JewelTheme.typography.h2TextStyle)
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
course.archived.forEach { EditionCard(course.course.name, it, vm, onOpenEdition) }
}
@@ -103,31 +106,31 @@ fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onO
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, 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 = MaterialTheme.typography.headlineSmall)
Text(edition.edition.name, style = JewelTheme.typography.h2TextStyle)
Text(
"$type\n${edition.students.size} student(s) • ${edition.groups.size} group(s) • ${edition.assignments.size} assignment(s)",
style = MaterialTheme.typography.bodyMedium
style = JewelTheme.typography.regular
)
}
Spacer(Modifier.height(5.dp))
Row {
if(edition.edition.archived) {
Button({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
DefaultButton({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Unarchive, "Unarchive edition")
Spacer(Modifier.width(5.dp))
Text("Unarchive edition")
}
}
else {
Button({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
DefaultButton({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
Icon(Archive, "Archive edition")
Spacer(Modifier.width(5.dp))
Text("Archive edition")
}
}
Spacer(Modifier.width(10.dp))
Button({ deleting = true }, Modifier.weight(0.5f)) {
DefaultButton({ deleting = true }, Modifier.weight(0.5f)) {
Icon(Delete, "Archive edition")
Spacer(Modifier.width(5.dp))
Text("Delete edition")

View File

@@ -1,36 +1,14 @@
package com.jaytux.grader.ui
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.material.ScrollableTabRow
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
@@ -42,19 +20,18 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading
import com.jaytux.grader.PeerEvalGrading
import com.jaytux.grader.app
import com.jaytux.grader.data.v2.CategoricGrade
import com.jaytux.grader.data.v2.GradeType
import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.viewmodel.Grade
import com.jaytux.grader.viewmodel.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator
import com.jaytux.grader.viewmodel.PeerEvalsGradingVM
import sun.tools.jconsole.LabeledComponent.layout
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalTextStyle
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.theme.colorPalette
import org.jetbrains.jewel.ui.typography
@Composable
fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading")
@@ -77,7 +54,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
}
Column(Modifier.padding(10.dp)) {
Text("Grading ${vm.base.name}", style = MaterialTheme.typography.headlineMedium)
Text("Grading ${vm.base.name}", style = JewelTheme.typography.h2TextStyle)
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) {
@@ -99,7 +76,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
Icon(DoubleBack, "Previous group")
}
Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall)
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")
@@ -261,13 +238,13 @@ Column(Modifier.padding(10.dp).fillMaxHeight()) {
var grade by remember(evaluator, evaluatee, current) { mutableStateOf(gradeState(critData, current?.grade)) }
var text by remember(evaluator, evaluatee, current) { mutableStateOf(current?.feedback ?: "") }
Text(evaluatee, style = MaterialTheme.typography.headlineSmall)
Text("Evaluated by $evaluator", style = MaterialTheme.typography.bodyMedium, fontStyle = FontStyle.Italic)
Text(evaluatee, style = JewelTheme.typography.h2TextStyle)
Text("Evaluated by $evaluator", style = JewelTheme.typography.regular, fontStyle = FontStyle.Italic)
Spacer(Modifier.height(10.dp))
GradePicker(grade, key = evaluator to evaluatee to current) { grade = it }
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 10, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(10.dp))
Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
DefaultButton({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save")
}
}
@@ -281,7 +258,7 @@ fun SingleStudentGrade(name: String, current: FeedbackItem?, critData: CritData,
Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp))
Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
DefaultButton({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save grade and feedback")
}
}

View File

@@ -4,7 +4,8 @@ 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.*
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
@@ -12,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
@@ -25,6 +25,8 @@ 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.LocalContentColor
import org.jetbrains.jewel.ui.component.*
@Composable
fun RichTextStyleRow(
@@ -211,13 +213,13 @@ fun RichTextStyleButton(
// (Happens only on Desktop)
.focusProperties { canFocus = false },
onClick = onClick,
colors = IconButtonDefaults.iconButtonColors(
contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onBackground
},
),
// colors = IconButtonDefaults.iconButtonColors(
// contentColor = if (isSelected) {
// MaterialTheme.colorScheme.onPrimary
// } else {
// MaterialTheme.colorScheme.onBackground
// },
// ),
) {
Icon(
icon,

View File

@@ -1,6 +1,5 @@
package com.jaytux.grader.ui
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading
@@ -8,6 +7,7 @@ import com.jaytux.grader.SoloGrading
import com.jaytux.grader.viewmodel.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator
import com.jaytux.grader.viewmodel.SolosGradingVM
import org.jetbrains.jewel.ui.component.Text
@Composable
fun SolosGradingTitle(data: SoloGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Individual Assignments / ${data.assignment.name} / Grading")

View File

@@ -16,15 +16,8 @@ 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.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -37,11 +30,14 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.data.v2.Edition
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.startEmail
import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.SnackVM
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalTextStyle
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@@ -69,7 +65,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) {
Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall)
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())
@@ -109,7 +105,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
}
Column {
Text("Groups:", style = MaterialTheme.typography.headlineSmall)
Text("Groups:", style = JewelTheme.typography.h2TextStyle)
groups?.let { gList ->
if(gList.isEmpty()) null
else {
@@ -142,7 +138,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
if(mod != students[focus].note) {
Row {
Spacer(Modifier.weight(1f))
Button({ vm.modStudent(students[focus], null, null, mod) }) {
DefaultButton({ vm.modStudent(students[focus], null, null, mod) }) {
Text("Update note")
}
}
@@ -150,7 +146,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
}
Spacer(Modifier.width(10.dp))
Column(Modifier.weight(0.66f)) {
Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall)
Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) {
LazyColumn {
item {

View File

@@ -1,200 +0,0 @@
package com.jaytux.grader.ui
//@Composable
//fun StudentView(state: StudentState, nav: Navigators) {
// val groups by state.groups.entities
// val courses by state.courseEditions.entities
// val groupGrades by state.groupGrades.entities
// val soloGrades by state.soloGrades.entities
//
// Column(Modifier.padding(10.dp)) {
// Row {
// Column(Modifier.weight(0.45f)) {
// Column(Modifier.padding(10.dp).weight(0.35f)) {
// Spacer(Modifier.height(10.dp))
// InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
// InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
// InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
// }
// Column(Modifier.weight(0.20f)) {
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
// val (ed, course) = it
// Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
// }
// }
// Column(Modifier.weight(0.45f)) {
// Text("Groups", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
// val (group, c) = it
// val (course, ed) = c
// Row(Modifier.clickable { nav.group(group) }) {
// Text(group.name, style = MaterialTheme.typography.bodyMedium)
// Spacer(Modifier.width(5.dp))
// Text(
// "(in course $course ($ed))",
// Modifier.align(Alignment.Bottom),
// style = MaterialTheme.typography.bodySmall
// )
// }
//
// }
// }
// }
// Column(Modifier.weight(0.55f)) {
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
// LazyColumn {
// item {
// Text("As group member", fontWeight = FontWeight.Bold)
// }
// items(groupGrades) {
// groupGradeWidget(it)
// }
//
// item {
// Text("Solo assignments", fontWeight = FontWeight.Bold)
// }
// items(soloGrades) {
// soloGradeWidget(it)
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun groupGradeWidget(gg: StudentState.LocalGroupGrade) {
// val (group, assignment, gGrade, iGrade) = gg
// var expanded by remember { mutableStateOf(false) }
// Row(Modifier.padding(5.dp)) {
// Spacer(Modifier.width(10.dp))
// Surface(
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
// tonalElevation = 5.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Column(Modifier.padding(5.dp)) {
// Text("${assignment.maxN(25)} (${iGrade ?: gGrade ?: "no grade yet"})")
//
// if (expanded) {
// Row {
// Spacer(Modifier.width(10.dp))
// Column {
// ItalicAndNormal("Assignment: ", assignment)
// ItalicAndNormal("Group name: ", group)
// ItalicAndNormal("Group grade: ", gGrade ?: "no grade yet")
// ItalicAndNormal("Individual grade: ", iGrade ?: "no individual grade")
// }
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
// val (assignment, grade) = sg
// var expanded by remember { mutableStateOf(false) }
// Row(Modifier.padding(5.dp)) {
// Spacer(Modifier.width(10.dp))
// Surface(
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
// tonalElevation = 5.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Column(Modifier.padding(5.dp)) {
// Text("${assignment.maxN(25)} (${grade ?: "no grade yet"})")
//
// if (expanded) {
// Row {
// Spacer(Modifier.width(10.dp))
// Column {
// ItalicAndNormal("Assignment: ", assignment)
// ItalicAndNormal("Individual grade: ", grade ?: "no grade yet")
// }
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun GroupView(state: GroupState, nav: Navigators) {
// val members by state.members.entities
// val available by state.availableStudents.entities
// val allRoles by state.roles.entities
//
// var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
//
// Column(Modifier.padding(10.dp)) {
// Row {
// Column(Modifier.weight(0.5f)) {
// Text("Students", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
// val (student, role) = it
// Row(Modifier.clickable { nav.student(student) }) {
// Text(
// "${student.name} (${role ?: "no role"})",
// Modifier.weight(0.75f).align(Alignment.CenterVertically),
// style = MaterialTheme.typography.bodyMedium
// )
// IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
// Icon(Icons.Default.Edit, "Change role")
// }
// IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
// Icon(Icons.Default.Delete, "Remove student")
// }
// }
// }
// }
// Column(Modifier.weight(0.5f)) {
// Text("Available students", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(available, { Text("No students available") }) { _, it ->
// Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
// IconButton({ state.addStudent(it) }) {
// Icon(ChevronLeft, "Add student")
// }
// Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
// }
// }
// }
// }
// }
//
// pickRole?.let {
// val (curr, onPick) = it
// RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
// }
//}
//
//@Composable
//fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
// onCloseRequest = onClose,
// state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
//) {
// Surface(Modifier.fillMaxSize().padding(10.dp)) {
// Box(Modifier.fillMaxSize()) {
// var role by remember { mutableStateOf(curr ?: "") }
// Column {
// Text("Used roles:")
// LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
// items(used) {
// Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
// Text(it, Modifier.padding(5.dp))
// }
// Spacer(Modifier.height(5.dp))
// }
// }
// OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
// CancelSaveRow(true, onClose) {
// onSave(role.ifBlank { null })
// onClose()
// }
// }
// }
// }
//}

View File

@@ -7,7 +7,13 @@ 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.material3.*
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.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -26,16 +32,18 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.Grade
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import java.util.*
import kotlin.time.toJavaInstant
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.Outline
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
Row {
Button({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) }
DefaultButton({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) }
Spacer(Modifier.weight(0.1f))
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) }
DefaultButton({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) }
}
}
@@ -224,7 +232,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
}
Row {
Spacer(Modifier.weight(1f))
Text(grade.value.option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall)
Text(grade.value.option, fontStyle = FontStyle.Italic, style = JewelTheme.typography.small)
}
}
}
@@ -240,7 +248,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
)
Row {
Spacer(Modifier.weight(1f))
Text(grade.options[slider].option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall)
Text(grade.options[slider].option, fontStyle = FontStyle.Italic, style = JewelTheme.typography.small)
}
}
}
@@ -250,7 +258,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
var text by remember(grade, key) { mutableStateOf(grade.text) }
OutlinedTextField(grade.text, { onUpdate(grade.copy(text = it)) }, Modifier.weight(1f), singleLine = true)
Button({ onUpdate(Grade.FreeText(text)) }, enabled = text != grade.text) {
DefaultButton({ onUpdate(Grade.FreeText(text)) }, enabled = text != grade.text) {
Text("Save")
}
}
@@ -261,7 +269,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
num, { num = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } },
Modifier.weight(1f), singleLine = true, isError = (num.toDoubleOrNull() ?: 0.0) > grade.grade.max
)
Button({ onUpdate(Grade.Numeric(num.toDoubleOrNull() ?: 0.0, grade.grade)) }, enabled = (num.toDoubleOrNull() ?: 0.0) <= grade.grade.max) {
DefaultButton({ onUpdate(Grade.Numeric(num.toDoubleOrNull() ?: 0.0, grade.grade)) }, enabled = (num.toDoubleOrNull() ?: 0.0) <= grade.grade.max) {
Text("Save")
}
}
@@ -269,9 +277,40 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
var perc by remember(grade, key) { mutableStateOf(grade.percentage.toString()) }
OutlinedTextField("$perc%", { perc = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } }, Modifier.weight(1f), singleLine = true)
Button({ onUpdate(Grade.Percentage(perc.toDoubleOrNull() ?: 0.0)) }) {
DefaultButton({ onUpdate(Grade.Percentage(perc.toDoubleOrNull() ?: 0.0)) }) {
Text("Save")
}
}
}
}
// 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) }
LaunchedEffect(value) {
if (state.text.toString() != value) {
state.edit {
replace(0, length, value)
}
}
}
LaunchedEffect(state) {
snapshotFlow { state.text }.collect { newText ->
val newString = newText.toString()
if (newString != value) {
onChange(newString)
}
}
}
if(singleLine) {
TextField(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label)
}
else {
TextArea(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label /*, minLines = minLines*/)
}
}

View File

@@ -12,6 +12,7 @@ rtf = "1.0.0-rc11"
filekit = "0.10.0-beta04"
directories = "26"
androidx-activity-compose = "1.12.2"
jewel = "0.34.0-253.31033.149"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -41,6 +42,7 @@ filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", vers
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
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" }
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }