Compare commits

..

2 Commits

Author SHA1 Message Date
28c3b29c3a Jewel-ize part II 2026-03-29 16:06:33 +02:00
18a7a82c36 Jewel-ize part I 2026-03-26 15:02:00 +01:00
22 changed files with 490 additions and 1787 deletions

View File

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

View File

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

View File

@@ -1,17 +1,12 @@
package com.jaytux.grader 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.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import com.jaytux.grader.data.Database
import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.awt.Desktop import java.awt.Desktop
import java.net.URI import java.net.URI
import java.time.Clock
import java.time.LocalDateTime
import java.util.prefs.Preferences import java.util.prefs.Preferences
fun String.maxN(n: Int): String { 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.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import com.jaytux.grader.App
import com.jaytux.grader.data.Database import com.jaytux.grader.data.Database
import io.github.vinceglb.filekit.FileKit 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,14 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.* import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.TimeInput
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -29,6 +36,9 @@ import kotlinx.datetime.*
import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.char import kotlinx.datetime.format.char
import kotlin.time.Instant import kotlin.time.Instant
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable @Composable
fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fillMaxSize()) { fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fillMaxSize()) {
@@ -57,13 +67,13 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
} }
} }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it ->
QuickAssignment(idx, it, vm) QuickAssignment(idx, it, vm)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (assignment == null) { if (assignment == null) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Text("Select an assignment to see details.", Modifier.padding(10.dp).align(Alignment.Center), fontStyle = FontStyle.Italic) Text("Select an assignment to see details.", Modifier.padding(10.dp).align(Alignment.Center), fontStyle = FontStyle.Italic)
@@ -73,11 +83,11 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
val peerEvalData by vm.asPeerEvaluation.entity val peerEvalData by vm.asPeerEvaluation.entity
var updatingPeerEvalGrade by remember { mutableStateOf(false) } 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) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic)
Row { Row {
Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically)) Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.clickable { updatingGrade = true }.padding(3.dp)) { Box(Modifier.clickable { updatingGrade = true }.padding(3.dp)) {
Text(when(val t = assignment.global.gradeType){ Text(when(val t = assignment.global.gradeType){
is UiGradeType.Categoric -> t.grade.name is UiGradeType.Categoric -> t.grade.name
@@ -92,7 +102,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
peerEvalData?.let { pe -> peerEvalData?.let { pe ->
Row { Row {
Text("Students are reviewing each other using ", Modifier.align(Alignment.CenterVertically)) Text("Students are reviewing each other using ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.clickable { updatingPeerEvalGrade = true }.padding(3.dp)) { Box(Modifier.clickable { updatingPeerEvalGrade = true }.padding(3.dp)) {
Text( Text(
when (val t = pe.second) { when (val t = pe.second) {
@@ -115,7 +125,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Row { Row {
Column(Modifier.weight(0.75f)) { Column(Modifier.weight(0.75f)) {
Row { 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()) }) { Button({ vm.setDesc(assignment, descRtf.toMarkdown()) }) {
Text("Update") Text("Update")
} }
@@ -127,9 +137,9 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Surface(Modifier.weight(0.25f), color = Color.White) { Surface(Modifier.weight(0.25f), color = Color.White) {
Column(Modifier.padding(15.dp)) { Column(Modifier.padding(15.dp)) {
Row { Row {
Text("Grading Rubrics", Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall) Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
IconButton({ addingRubric = true }) { IconButton({ addingRubric = true }) {
Icon(CirclePlus, "Add grading rubric") Icon(Icons.CirclePlus, "Add grading rubric")
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
@@ -141,7 +151,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
Text(it.criterion.desc, Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text(it.criterion.desc, Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
} }
IconButton({ editingRubric = idx }, Modifier.align(Alignment.Top)) { IconButton({ editingRubric = idx }, Modifier.align(Alignment.Top)) {
Icon(Edit, "Edit grading rubric") Icon(Icons.Edit, "Edit grading rubric")
} }
} }
} }
@@ -207,7 +217,7 @@ val fmt = LocalDateTime.Format {
@Composable @Composable
fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) { fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) {
val focus by vm.focusIndex val focus by vm.focusIndex
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = focus == idx, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
Text(assignment.assignment.name, fontWeight = FontWeight.Bold) Text(assignment.assignment.name, fontWeight = FontWeight.Bold)
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
@@ -262,7 +272,7 @@ fun DeadlinePicker(deadline: LocalDateTime, onDismiss: () -> Unit, onSave: (Loca
} }
Dialog(onDismiss, DialogProperties()) { Dialog(onDismiss, DialogProperties()) {
Surface(tonalElevation = 5.dp, shape = MaterialTheme.shapes.extraLarge) { Surface(shape = JewelTheme.shapes.large) {
Column(Modifier.padding(15.dp)) { Column(Modifier.padding(15.dp)) {
DatePicker(state, Modifier.fillMaxWidth()) DatePicker(state, Modifier.fillMaxWidth())
TimeInput(time, Modifier.fillMaxWidth()) TimeInput(time, Modifier.fillMaxWidth())
@@ -298,7 +308,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken:
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Criterion Name") }, isError = name in taken, singleLine = true) OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Criterion Name") }, isError = name in taken, singleLine = true)
OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true) OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true)
Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Surface(shape = JewelTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
@@ -328,8 +338,8 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
Text("Select a grading scale for $name", style = 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)) { Surface(shape = JewelTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
@@ -397,8 +407,8 @@ fun GradeTypePicker(
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(categories) { idx, it -> itemsIndexed(categories) { idx, it ->
Surface( Surface(
tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp, markFocused = selectedCategory == idx,
shape = MaterialTheme.shapes.small shape = JewelTheme.shapes.small
) { ) {
Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
Text(it.grade.name, fontWeight = FontWeight.Bold) Text(it.grade.name, fontWeight = FontWeight.Bold)
@@ -421,8 +431,8 @@ fun GradeTypePicker(
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(numeric) { idx, it -> itemsIndexed(numeric) { idx, it ->
Surface( Surface(
tonalElevation = if (selectedNumeric == idx) 15.dp else 0.dp, markFocused = selectedNumeric == idx,
shape = MaterialTheme.shapes.small shape = JewelTheme.shapes.small
) { ) {
Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) {
Text(it.grade.name, fontWeight = FontWeight.Bold) Text(it.grade.name, fontWeight = FontWeight.Bold)
@@ -470,13 +480,13 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Grading system name") }, isError = name in taken, singleLine = true) 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)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(options) { idx, it -> itemsIndexed(options) { idx, it ->
Row(Modifier.fillMaxWidth().padding(5.dp)) { Row(Modifier.fillMaxWidth().padding(5.dp)) {
Text(it, Modifier.weight(1f)) Text(it, Modifier.weight(1f))
IconButton({ options = options.filterNot { o -> o == it } }) { IconButton({ options = options.filterNot { o -> o == it } }) {
Icon(Delete, "Delete grading option") Icon(Icons.Delete, "Delete grading option")
} }
} }
} }

View File

@@ -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 package com.jaytux.grader.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.runtime.*
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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.EditionDetail 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.EditionVM
import com.jaytux.grader.viewmodel.Navigator 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 @Composable
fun EditionTitle(data: EditionDetail) = Text("Courses / ${data.course.name} / ${data.ed.name}") fun EditionTitle(data: EditionDetail) = Text("Courses / ${data.course.name} / ${data.ed.name}")
@@ -44,9 +29,9 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row { Row {
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = MaterialTheme.typography.headlineMedium) Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle)
Button({ adding = true }) { IconButton({ adding = true }) {
Icon(CirclePlus, "Add ${tab.addText}") Icon(Icons.CirclePlus, "Add ${tab.addText}")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Add ${tab.addText}") Text("Add ${tab.addText}")
} }
@@ -80,21 +65,21 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
@Composable @Composable
fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserIcon, "Students") Icon(Icons.UserIcon, "Students")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Students") Text("Students")
} }
@Composable @Composable
fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(UserGroupIcon, "Groups") Icon(Icons.UserGroupIcon, "Groups")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Groups") Text("Groups")
} }
@Composable @Composable
fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) { fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
Icon(AssignmentIcon, "Assignments") Icon(Icons.AssignmentIcon, "Assignments")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text("Assignments") Text("Assignments")
} }

View File

@@ -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,10 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button import androidx.compose.runtime.*
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.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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -40,16 +14,13 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading import com.jaytux.grader.GroupGrading
import com.jaytux.grader.app 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.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.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.Navigator
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import java.util.*
import java.util.UUID import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.typography
@Composable @Composable
fun GroupsGradingTitle(data: GroupGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Group Assignments / ${data.assignment.name} / Grading") fun GroupsGradingTitle(data: GroupGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Group Assignments / ${data.assignment.name} / Grading")
@@ -65,17 +36,17 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) } val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) }
Column(Modifier.padding(10.dp)) { 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}") Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) { Row(Modifier.fillMaxSize()) {
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it) QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (focus == -1 || selectedGroup == null) { if (focus == -1 || selectedGroup == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to start grading.", Modifier.align(Alignment.Center)) Text("Select a group to start grading.", Modifier.align(Alignment.Center))
@@ -84,13 +55,13 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Row { Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) { IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group") Icon(Icons.DoubleBack, "Previous group")
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall) Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
Icon(DoubleForward, "Next group") Icon(Icons.DoubleForward, "Next group")
} }
} }
@@ -99,7 +70,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
val global by vm.globalGrade.entity val global by vm.globalGrade.entity
val byCriteria by vm.gradeList.entities val byCriteria by vm.gradeList.entities
Surface(Modifier.fillMaxSize(), color = Color.White, shape = MaterialTheme.shapes.medium) { Surface(Modifier.fillMaxSize(), color = Color.White, shape = JewelTheme.shapes.medium) {
LazyColumn { LazyColumn {
items(byCriteria ?: listOf()) { (crit, fdbk) -> items(byCriteria ?: listOf()) { (crit, fdbk) ->
var isOpen by remember(selectedGroup) { mutableStateOf(false) } var isOpen by remember(selectedGroup) { mutableStateOf(false) }
@@ -133,7 +104,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
@Composable @Composable
fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.GroupData) { fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.GroupData) {
Surface(tonalElevation = if(isFocus) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = isFocus, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { onFocus() }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { onFocus() }.padding(10.dp)) {
Text(group.group.name, fontWeight = FontWeight.Bold) Text(group.group.name, fontWeight = FontWeight.Bold)
Text("${group.students.size} student(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) Text("${group.students.size} student(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
@@ -146,23 +117,23 @@ fun GFWidget(
crit: CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any, crit: CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any,
isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(), isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(),
onToggle: () -> Unit onToggle: () -> Unit
) = Surface(Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) { ) = Surface(Modifier.fillMaxWidth(), shape = JewelTheme.shapes.medium) {
Column { Column {
Surface(tonalElevation = 5.dp) { Surface {
Row(Modifier.fillMaxWidth().clickable { onToggle() }.padding(10.dp)) { Row(Modifier.fillMaxWidth().clickable { onToggle() }.padding(10.dp)) {
Icon(if(isOpen) ChevronDown else ChevronRight, "Toggle criterion detail grading", Modifier.align(Alignment.CenterVertically)) Icon(if(isOpen) Icons.ChevronDown else Icons.ChevronRight, "Toggle criterion detail grading", Modifier.align(Alignment.CenterVertically))
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Column(Modifier.align(Alignment.CenterVertically)) { Column(Modifier.align(Alignment.CenterVertically)) {
Row { Row {
Text(overrideName ?: crit.criterion.name, style = MaterialTheme.typography.bodyLarge) Text(overrideName ?: crit.criterion.name, style = JewelTheme.typography.h4TextStyle)
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
feedback.groupLevel?.grade?.let { feedback.groupLevel?.grade?.let {
Row(Modifier.align(Alignment.Bottom)) { Row(Modifier.align(Alignment.Bottom)) {
ProvideTextStyle(MaterialTheme.typography.bodySmall) { // ProvideTextStyle(JewelTheme.typography.small) {
Text("(Grade: ") Text("(Grade: ")
it.render() it.render()
Text(")") Text(")")
} // }
} }
} }
} }
@@ -183,7 +154,7 @@ fun GFWidget(
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f)) OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp)) 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") Text("Save grade and feedback")
} }
} }
@@ -191,9 +162,9 @@ fun GFWidget(
feedback.groupLevel?.let { groupLevel -> feedback.groupLevel?.let { groupLevel ->
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), tonalElevation = 10.dp, shape = MaterialTheme.shapes.small) { Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), shape = JewelTheme.shapes.small) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Text("Individual overrides", style = MaterialTheme.typography.bodyLarge) Text("Individual overrides", style = JewelTheme.typography.h4TextStyle)
feedback.overrides.forEach { (student, it) -> feedback.overrides.forEach { (student, it) ->
var enable by remember(key, it) { mutableStateOf(it != null) } var enable by remember(key, it) { mutableStateOf(it != null) }
var maybeRemoving by remember(key, it) { mutableStateOf(false) } var maybeRemoving by remember(key, it) { mutableStateOf(false) }
@@ -207,20 +178,20 @@ fun GFWidget(
Text(student.name, Modifier.align(Alignment.CenterVertically)) Text(student.name, Modifier.align(Alignment.CenterVertically))
if(student.id.value in markOverridden) { if(student.id.value in markOverridden) {
Spacer(Modifier.width(5.dp)) 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)
} }
} }
if(enable) Row { if(enable) Row {
Spacer(Modifier.width(15.dp)) Spacer(Modifier.width(15.dp))
Surface(color = Color.White, shape = MaterialTheme.shapes.small) { Surface(color = Color.White, shape = JewelTheme.shapes.small) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
GradePicker(sGrade, key = crit to gr app student) { sGrade = it } GradePicker(sGrade, key = crit to gr app student) { sGrade = it }
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
OutlinedTextField(sText, { sText = it }, label = { Text("Feedback") }, singleLine = true, modifier = Modifier.fillMaxWidth()) OutlinedTextField(sText, { sText = it }, label = { Text("Feedback") }, singleLine = true, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(5.dp)) 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") Text("Save override")
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,12 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.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.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.runtime.*
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
@@ -42,19 +18,18 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading
import com.jaytux.grader.PeerEvalGrading import com.jaytux.grader.PeerEvalGrading
import com.jaytux.grader.app 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.Group
import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.data.v2.Student import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.viewmodel.Grade import com.jaytux.grader.viewmodel.Grade
import com.jaytux.grader.viewmodel.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.Navigator
import com.jaytux.grader.viewmodel.PeerEvalsGradingVM 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 @Composable
fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading") fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading")
@@ -77,17 +52,17 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
} }
Column(Modifier.padding(10.dp)) { 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}") Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) { Row(Modifier.fillMaxSize()) {
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it) QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if (focus == -1 || selectedGroup == null) { if (focus == -1 || selectedGroup == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to start grading.", Modifier.align(Alignment.Center)) Text("Select a group to start grading.", Modifier.align(Alignment.Center))
@@ -96,13 +71,13 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Row { Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) { IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group") Icon(Icons.DoubleBack, "Previous group")
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall) Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
Icon(DoubleForward, "Next group") Icon(Icons.DoubleForward, "Next group")
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
@@ -113,7 +88,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
} }
} }
} ?: Box(Modifier.weight(0.66f).fillMaxWidth()) { } ?: Box(Modifier.weight(0.66f).fillMaxWidth()) {
Text("Error: could not load evaluations for this group.", Modifier.align(Alignment.Center), color = MaterialTheme.colorScheme.error) Text("Error: could not load evaluations for this group.", Modifier.align(Alignment.Center), color = JewelTheme.globalColors.text.error)
} }
Column(Modifier.weight(0.33f)) { Column(Modifier.weight(0.33f)) {
@@ -124,7 +99,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
sgs.forEachIndexed { idx, st -> sgs.forEachIndexed { idx, st ->
Tab(idx == selectedStudent, { selectedStudent = idx }) { Tab(idx == selectedStudent, { selectedStudent = idx }) {
Row { Row {
Icon(UserIcon, "") Icon(Icons.UserIcon, "")
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Text(st.first.name, Modifier.align(Alignment.CenterVertically)) Text(st.first.name, Modifier.align(Alignment.CenterVertically))
} }
@@ -244,7 +219,7 @@ fun GradeTable(
} }
editing?.let { editing?.let {
Surface(Modifier.weight(0.33f), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.weight(0.33f), shape = JewelTheme.shapes.medium) {
val (evaluator, evaluatee, data) = it val (evaluator, evaluatee, data) = it
EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { grade, feedback -> EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { grade, feedback ->
onSet(evaluator, evaluatee, group, grade, feedback) onSet(evaluator, evaluatee, group, grade, feedback)
@@ -261,13 +236,13 @@ Column(Modifier.padding(10.dp).fillMaxHeight()) {
var grade by remember(evaluator, evaluatee, current) { mutableStateOf(gradeState(critData, current?.grade)) } var grade by remember(evaluator, evaluatee, current) { mutableStateOf(gradeState(critData, current?.grade)) }
var text by remember(evaluator, evaluatee, current) { mutableStateOf(current?.feedback ?: "") } var text by remember(evaluator, evaluatee, current) { mutableStateOf(current?.feedback ?: "") }
Text(evaluatee, style = MaterialTheme.typography.headlineSmall) Text(evaluatee, style = JewelTheme.typography.h2TextStyle)
Text("Evaluated by $evaluator", style = MaterialTheme.typography.bodyMedium, fontStyle = FontStyle.Italic) Text("Evaluated by $evaluator", style = JewelTheme.typography.regular, fontStyle = FontStyle.Italic)
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
GradePicker(grade, key = evaluator to evaluatee to current) { grade = it } GradePicker(grade, key = evaluator to evaluatee to current) { grade = it }
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 10, modifier = Modifier.fillMaxWidth()) OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 10, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(10.dp)) 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") Text("Save")
} }
} }
@@ -281,7 +256,7 @@ fun SingleStudentGrade(name: String, current: FeedbackItem?, critData: CritData,
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f)) OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp)) 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") Text("Save grade and feedback")
} }
} }

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -12,7 +11,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@@ -25,6 +23,11 @@ import com.jaytux.grader.toClipboard
import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalContentColor
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
import org.jetbrains.jewel.ui.theme.iconButtonStyle
@Composable @Composable
fun RichTextStyleRow( fun RichTextStyleRow(
@@ -49,7 +52,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold, isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
icon = FormatBold icon = Icons.FormatBold
) )
} }
@@ -63,7 +66,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic, isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic,
icon = FormatItalic icon = Icons.FormatItalic
) )
} }
@@ -77,7 +80,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true,
icon = FormatUnderline icon = Icons.FormatUnderline
) )
} }
@@ -91,7 +94,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true, isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true,
icon = FormatStrikethrough icon = Icons.FormatStrikethrough
) )
} }
@@ -105,7 +108,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.fontSize == 28.sp, isSelected = state.currentSpanStyle.fontSize == 28.sp,
icon = FormatSize icon = Icons.FormatSize
) )
} }
@@ -119,7 +122,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.color == Color.Red, isSelected = state.currentSpanStyle.color == Color.Red,
icon = CircleFilled, icon = Icons.CircleFilled,
tint = Color.Red tint = Color.Red
) )
} }
@@ -134,7 +137,7 @@ fun RichTextStyleRow(
) )
}, },
isSelected = state.currentSpanStyle.background == Color.Yellow, isSelected = state.currentSpanStyle.background == Color.Yellow,
icon = CircleOutline, icon = Icons.CircleOutline,
tint = Color.Yellow tint = Color.Yellow
) )
} }
@@ -154,7 +157,7 @@ fun RichTextStyleRow(
state.toggleUnorderedList() state.toggleUnorderedList()
}, },
isSelected = state.isUnorderedList, isSelected = state.isUnorderedList,
icon = FormatListBullet, icon = Icons.FormatListBullet,
) )
} }
@@ -164,7 +167,7 @@ fun RichTextStyleRow(
state.toggleOrderedList() state.toggleOrderedList()
}, },
isSelected = state.isOrderedList, isSelected = state.isOrderedList,
icon = FormatListNumber, icon = Icons.FormatListNumber,
) )
} }
@@ -183,16 +186,16 @@ fun RichTextStyleRow(
state.toggleCodeSpan() state.toggleCodeSpan()
}, },
isSelected = state.isCodeSpan, isSelected = state.isCodeSpan,
icon = FormatCode, icon = Icons.FormatCode,
) )
} }
} }
IconButton({ scope.launch { state.toClipboard(clip) } }) { IconButton({ scope.launch { state.toClipboard(clip) } }) {
Icon(ContentCopy, contentDescription = "Copy markdown") Icon(Icons.ContentCopy, contentDescription = "Copy markdown")
} }
IconButton({ scope.launch { state.loadClipboard(clip, scope) } }) { IconButton({ scope.launch { state.loadClipboard(clip, scope) } }) {
Icon(ContentPaste, contentDescription = "Paste markdown") Icon(Icons.ContentPaste, contentDescription = "Paste markdown")
} }
} }
} }
@@ -211,13 +214,7 @@ fun RichTextStyleButton(
// (Happens only on Desktop) // (Happens only on Desktop)
.focusProperties { canFocus = false }, .focusProperties { canFocus = false },
onClick = onClick, onClick = onClick,
colors = IconButtonDefaults.iconButtonColors( style = IconButtonStyle(JewelTheme.iconButtonStyle.colors, JewelTheme.iconButtonStyle.metrics) // TODO: color swapping depending on isSelected
contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onBackground
},
),
) { ) {
Icon( Icon(
icon, icon,
@@ -226,7 +223,7 @@ fun RichTextStyleButton(
modifier = Modifier modifier = Modifier
.background( .background(
color = if (isSelected) { color = if (isSelected) {
MaterialTheme.colorScheme.primary JewelTheme.globalColors.text.disabledSelected
} else { } else {
Color.Transparent Color.Transparent
}, },

View File

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

View File

@@ -16,15 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -37,11 +28,14 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.data.v2.Edition
import com.jaytux.grader.data.v2.Student import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.startEmail import com.jaytux.grader.startEmail
import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.SnackVM 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 @Composable
fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@@ -49,13 +43,13 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val focus by vm.focusIndex val focus by vm.focusIndex
val snacks = viewModel<SnackVM> { SnackVM() } val snacks = viewModel<SnackVM> { SnackVM() }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight()) {
ListOrEmpty(students, { Text("No students yet.") }) { idx, it -> ListOrEmpty(students, { Text("No students yet.") }) { idx, it ->
QuickStudent(idx, it, vm) QuickStudent(idx, it, vm)
} }
} }
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) { Surface(Modifier.weight(0.75f).fillMaxHeight()) {
if(focus == -1) { if(focus == -1) {
Box(Modifier.weight(0.75f).fillMaxHeight()) { Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a student to view details.", Modifier.align(Alignment.Center)) Text("Select a student to view details.", Modifier.align(Alignment.Center))
@@ -66,13 +60,13 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val grades by vm.studentGrades.entities val grades by vm.studentGrades.entities
Column(Modifier.weight(0.75f).padding(15.dp)) { Column(Modifier.weight(0.75f).padding(15.dp)) {
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.padding(10.dp).fillMaxWidth(), shape = JewelTheme.shapes.medium) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall) Text(students[focus].name, style = JewelTheme.typography.h2TextStyle)
if(students[focus].contact.isNotBlank()) { if(students[focus].contact.isNotBlank()) {
IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) { IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) {
Icon(Mail, "Send email", Modifier.fillMaxHeight()) Icon(Icons.Mail, "Send email", Modifier.fillMaxHeight())
} }
} }
} }
@@ -93,29 +87,29 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Text(students[focus].contact, Modifier.padding(start = 5.dp)) Text(students[focus].contact, Modifier.padding(start = 5.dp))
} }
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Edit, "Edit contact info", Modifier.clickable { editing = true }) Icon(Icons.Edit, "Edit contact info", Modifier.clickable { editing = true })
} }
else { else {
var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) } var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) }
OutlinedTextField(mod, { mod = it }) OutlinedTextField(mod, { mod = it })
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable { Icon(Icons.Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable {
vm.modStudent(students[focus], null, mod, null) vm.modStudent(students[focus], null, mod, null)
editing = false editing = false
}) })
Spacer(Modifier.width(5.dp)) Spacer(Modifier.width(5.dp))
Icon(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false }) Icon(Icons.Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false })
} }
} }
Column { Column {
Text("Groups:", style = MaterialTheme.typography.headlineSmall) Text("Groups:", style = JewelTheme.typography.h2TextStyle)
groups?.let { gList -> groups?.let { gList ->
if(gList.isEmpty()) null if(gList.isEmpty()) null
else { else {
FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) { FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
gList.forEach { group -> gList.forEach { group ->
Surface(tonalElevation = 15.dp, shadowElevation = 1.dp, shape = MaterialTheme.shapes.small) { Surface(shape = JewelTheme.shapes.small) {
Box(Modifier.padding(5.dp).clickable { vm.focus(group.first) }) { Box(Modifier.padding(5.dp).clickable { vm.focus(group.first) }) {
Text("${group.first.name} (${group.second ?: "no role"})", Modifier.padding(5.dp)) Text("${group.first.name} (${group.second ?: "no role"})", Modifier.padding(5.dp))
} }
@@ -142,7 +136,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
if(mod != students[focus].note) { if(mod != students[focus].note) {
Row { Row {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button({ vm.modStudent(students[focus], null, null, mod) }) { DefaultButton({ vm.modStudent(students[focus], null, null, mod) }) {
Text("Update note") Text("Update note")
} }
} }
@@ -150,11 +144,11 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Column(Modifier.weight(0.66f)) { Column(Modifier.weight(0.66f)) {
Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall) Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle)
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) { Surface(shape = JewelTheme.shapes.medium, color = Color.White) {
LazyColumn { LazyColumn {
item { item {
Surface(tonalElevation = 15.dp) { Surface {
Row(Modifier.padding(10.dp)) { Row(Modifier.padding(10.dp)) {
Text("Assignment", Modifier.weight(0.66f)) Text("Assignment", Modifier.weight(0.66f))
Text("Grade", Modifier.weight(0.33f)) Text("Grade", Modifier.weight(0.33f))
@@ -198,7 +192,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
@Composable @Composable
fun QuickStudent(idx: Int, student: Student, vm: EditionVM) { fun QuickStudent(idx: Int, student: Student, vm: EditionVM) {
val focus by vm.focusIndex val focus by vm.focusIndex
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) { Surface(markFocused = focus == idx, shape = JewelTheme.shapes.small) {
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) { Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
Text(student.name, fontWeight = FontWeight.Bold) Text(student.name, fontWeight = FontWeight.Bold)
if(student.contact.isBlank()) if(student.contact.isBlank())

View File

@@ -1,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

@@ -1,19 +1,32 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.* 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.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
@@ -26,16 +39,23 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.maxN import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.Grade import com.jaytux.grader.viewmodel.Grade
import kotlinx.datetime.* import kotlinx.datetime.*
import kotlinx.datetime.TimeZone import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import java.util.* import java.util.*
import kotlin.time.toJavaInstant 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 @Composable
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) { fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
Row { Row {
Button({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) } DefaultButton({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) }
Spacer(Modifier.weight(0.1f)) 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) }
} }
} }
@@ -72,7 +92,7 @@ fun ConfirmDeleteDialog(
onCloseRequest = onExit, onCloseRequest = onExit,
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center)) state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
) { ) {
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) { Surface(Modifier.width(400.dp).height(300.dp)) {
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {
Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp)) Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp))
@@ -145,8 +165,8 @@ fun Selectable(
) { ) {
Surface( Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) selectedElevation else unselectedElevation, markFocused = isSelected,
shape = MaterialTheme.shapes.medium shape = JewelTheme.shapes.medium
) { ) {
content() content()
} }
@@ -189,7 +209,7 @@ fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (
Text("Used roles:") Text("Used roles:")
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) { LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
items(used) { items(used) {
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) { Surface(Modifier.fillMaxWidth().clickable { role = it }) {
Text(it, Modifier.padding(5.dp)) Text(it, Modifier.padding(5.dp))
} }
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
@@ -224,7 +244,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
} }
Row { Row {
Spacer(Modifier.weight(1f)) 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 +260,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
) )
Row { Row {
Spacer(Modifier.weight(1f)) 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 +270,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
var text by remember(grade, key) { mutableStateOf(grade.text) } var text by remember(grade, key) { mutableStateOf(grade.text) }
OutlinedTextField(grade.text, { onUpdate(grade.copy(text = it)) }, Modifier.weight(1f), singleLine = true) 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") Text("Save")
} }
} }
@@ -261,7 +281,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
num, { num = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } }, num, { num = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } },
Modifier.weight(1f), singleLine = true, isError = (num.toDoubleOrNull() ?: 0.0) > grade.grade.max 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") Text("Save")
} }
} }
@@ -269,9 +289,138 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
var perc by remember(grade, key) { mutableStateOf(grade.percentage.toString()) } 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) 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") Text("Save")
} }
} }
} }
} }
@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*/)
}
}
private val LocalSurfaceLayer = compositionLocalOf { 0 }
interface ShapeCollection {
val xLarge: Shape
val large: Shape
val medium: Shape
val small: Shape
val xSmall: Shape
val none: Shape
}
val JewelTheme.Companion.shapes
get() = object : ShapeCollection {
override val xLarge = RoundedCornerShape(28.0.dp)
override val large = RoundedCornerShape(16.0.dp)
override val xSmall = RoundedCornerShape(4.0.dp)
override val medium = RoundedCornerShape(12.0.dp)
override val none = RectangleShape
override val small = RoundedCornerShape(8.0.dp)
}
@Composable
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
color: Color = JewelTheme.globalColors.panelBackground,
markFocused: Boolean = false,
content: @Composable () -> Unit
) {
val currentLayer = LocalSurfaceLayer.current
// TODO: markFocused?
Box(modifier = modifier.background(color, shape).let {
if (currentLayer > 0) it.border(Stroke(1.dp, JewelTheme.globalColors.outlines.focused, Stroke.Alignment.Center), shape)
else it
}) {
CompositionLocalProvider(LocalSurfaceLayer provides currentLayer + 1) { content() }
}
}
@Composable
fun Scaffold(topBar: @Composable () -> Unit, snackState: SnackbarHostState, content: @Composable () -> Unit) {
Column(Modifier.fillMaxSize()) {
Box(Modifier.heightIn(max = 150.dp)) {
CompositionLocalProvider(LocalTextStyle provides JewelTheme.typography.h1TextStyle) {
topBar()
}
}
Box(Modifier.weight(1f)) {
content()
}
}
NotificationHost(snackState)
}
@Composable
fun NotificationHost(host: SnackbarHostState) {
val currentData = host.currentSnackbarData
Box(Modifier.fillMaxSize()) {
if (currentData != null) {
Notification(
message = currentData.visuals.message,
onDismiss = { currentData.dismiss() },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
)
}
}
}
@Composable
fun Notification(message: String, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.widthIn(max = 300.dp).shadow(8.dp, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp)
) {
Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = message, modifier = Modifier.weight(1f), style = JewelTheme.defaultTextStyle)
IconButton(onClick = onDismiss) {
Icon(Icons.Close, contentDescription = "Close")
}
}
}
}
@Composable
fun TitleBar(modifier: Modifier = Modifier, title: @Composable () -> Unit, navigationIcon: (@Composable () -> Unit)? = null) {
Surface(modifier) {
Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 15.dp), verticalAlignment = Alignment.CenterVertically) {
if (navigationIcon != null) {
Box(Modifier.padding(end = 8.dp)) {
navigationIcon()
}
}
title()
}
}
}

View File

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

View File

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