From 18a7a82c36c80dd2906095d276d36d5a5d13b250 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Thu, 26 Mar 2026 15:02:00 +0100 Subject: [PATCH] Jewel-ize part I --- composeApp/build.gradle.kts | 5 +- .../kotlin/com/jaytux/grader/App.kt | 4 +- .../kotlin/com/jaytux/grader/Util.kt | 5 - .../kotlin/com/jaytux/grader/main.kt | 1 - .../com/jaytux/grader/ui/Assignments.kt | 715 ------------------ .../com/jaytux/grader/ui/AssignmentsView.kt | 25 +- .../kotlin/com/jaytux/grader/ui/Courses.kt | 87 --- .../com/jaytux/grader/ui/EditionView.kt | 29 +- .../kotlin/com/jaytux/grader/ui/Editions.kt | 409 ---------- .../com/jaytux/grader/ui/GroupsGradingView.kt | 58 +- .../kotlin/com/jaytux/grader/ui/GroupsView.kt | 18 +- .../kotlin/com/jaytux/grader/ui/HomeView.kt | 29 +- .../jaytux/grader/ui/PeerEvalsGradingView.kt | 49 +- .../kotlin/com/jaytux/grader/ui/RichText.kt | 20 +- .../com/jaytux/grader/ui/SolosGradingView.kt | 2 +- .../com/jaytux/grader/ui/StudentsView.kt | 20 +- .../kotlin/com/jaytux/grader/ui/Views.kt | 200 ----- .../kotlin/com/jaytux/grader/ui/Widgets.kt | 59 +- gradle/libs.versions.toml | 2 + 19 files changed, 157 insertions(+), 1580 deletions(-) delete mode 100644 composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt delete mode 100644 composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Courses.kt delete mode 100644 composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt delete mode 100644 composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 98e6288..8ac2c95 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -17,7 +17,9 @@ kotlin { val desktopMain by getting desktopMain.dependencies { - implementation(compose.desktop.currentOs) + implementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + } implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) @@ -45,6 +47,7 @@ kotlin { implementation(libs.directories) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.compose.backhandler) + implementation(libs.jewel) } } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/App.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/App.kt index 2c6d48b..32fdfd7 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/App.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/App.kt @@ -1,6 +1,5 @@ package com.jaytux.grader -import androidx.compose.material3.* import androidx.compose.runtime.* import com.jaytux.grader.data.v2.BaseAssignment import com.jaytux.grader.data.v2.Course @@ -16,6 +15,7 @@ import com.jaytux.grader.ui.PeerEvalsGradingView import com.jaytux.grader.ui.SolosGradingTitle import com.jaytux.grader.ui.SolosGradingView import com.jaytux.grader.viewmodel.Navigator +import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme object Home : Navigator.IDestination data class EditionDetail(val ed: Edition, val course: Course) : Navigator.IDestination @@ -25,7 +25,7 @@ data class PeerEvalGrading(val course: Course, val edition: Edition, val assignm @Composable fun App() { - MaterialTheme { + IntUiTheme(isDark = true) { Navigator.NavHost(Home) { composable({ HomeTitle() }) { _, token -> HomeView(token) } composable({ EditionTitle(it) }) { data, token -> EditionView(data, token) } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/Util.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/Util.kt index 6febaf7..cf83c80 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/Util.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/Util.kt @@ -1,17 +1,12 @@ package com.jaytux.grader -import androidx.compose.ui.platform.ClipEntry -import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.text.AnnotatedString -import com.jaytux.grader.data.Database import com.mohamedrejeb.richeditor.model.RichTextState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.awt.Desktop import java.net.URI -import java.time.Clock -import java.time.LocalDateTime import java.util.prefs.Preferences fun String.maxN(n: Int): String { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/main.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/main.kt index 0d4cd5d..9fc0edc 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/main.kt @@ -2,7 +2,6 @@ package com.jaytux.grader import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import com.jaytux.grader.App import com.jaytux.grader.data.Database import io.github.vinceglb.filekit.FileKit diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt deleted file mode 100644 index 13ffb46..0000000 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ /dev/null @@ -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, -// 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, -// currentCriterion: Int, -// onSelectCriterion: (Int) -> Unit, -// rawFeedback: GroupAssignmentState.FeedbackEntry?, -// autofill: List, -// onSave: (String, String) -> Unit, -// critGrades: List>? = null, -// groupCritGrades: List? = 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, -// currentCriterion: Int, -// onSelectCriterion: (Int) -> Unit, -// globFeedback: SoloAssignmentState.LocalFeedback?, -// criterionFeedback: SoloAssignmentState.LocalFeedback?, -// autofill: List, -// 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?>(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 -// ) -// } -// } -// } -// } -//} diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt index 57ecb5c..4b93889 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt @@ -4,7 +4,17 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.DatePicker +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface +import androidx.compose.material3.TimeInput +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -29,6 +39,9 @@ import kotlinx.datetime.* import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.char import kotlin.time.Instant +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fillMaxSize()) { @@ -73,7 +86,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil val peerEvalData by vm.asPeerEvaluation.entity var updatingPeerEvalGrade by remember { mutableStateOf(false) } - Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium) + Text(assignment.assignment.name, style = JewelTheme.typography.h2TextStyle) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic) Row { Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically)) @@ -115,7 +128,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil Row { Column(Modifier.weight(0.75f)) { Row { - Text("Description:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp).weight(1f)) + Text("Description:", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(top = 10.dp).weight(1f)) Button({ vm.setDesc(assignment, descRtf.toMarkdown()) }) { Text("Update") } @@ -127,7 +140,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil Surface(Modifier.weight(0.25f), color = Color.White) { Column(Modifier.padding(15.dp)) { Row { - Text("Grading Rubrics", Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall) + Text("Grading Rubrics", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle) IconButton({ addingRubric = true }) { Icon(CirclePlus, "Add grading rubric") } @@ -328,7 +341,7 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose: Surface(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize().padding(10.dp)) { Column(Modifier.align(Alignment.Center)) { - Text("Select a grading scale for $name", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 10.dp)) + Text("Select a grading scale for $name", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(bottom = 10.dp)) Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Column { GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } @@ -470,7 +483,7 @@ fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, Box(Modifier.fillMaxSize().padding(10.dp)) { Column(Modifier.align(Alignment.Center)) { OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Grading system name") }, isError = name in taken, singleLine = true) - Text("Grade options:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp)) + Text("Grade options:", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(top = 10.dp)) LazyColumn(Modifier.weight(1f)) { itemsIndexed(options) { idx, it -> Row(Modifier.fillMaxWidth().padding(5.dp)) { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Courses.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Courses.kt deleted file mode 100644 index 6918524..0000000 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Courses.kt +++ /dev/null @@ -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") } -// } -// } -//} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/EditionView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/EditionView.kt index f279c65..e05672c 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/EditionView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/EditionView.kt @@ -1,33 +1,18 @@ package com.jaytux.grader.ui -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.* import androidx.compose.material3.PrimaryScrollableTabRow -import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.jaytux.grader.EditionDetail -import com.jaytux.grader.data.v2.BaseAssignment -import com.jaytux.grader.data.v2.Student import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.Navigator +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun EditionTitle(data: EditionDetail) = Text("Courses / ${data.course.name} / ${data.ed.name}") @@ -44,8 +29,8 @@ fun EditionView(data: EditionDetail, token: Navigator.NavToken) { Column(Modifier.padding(10.dp)) { Row { - Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = MaterialTheme.typography.headlineMedium) - Button({ adding = true }) { + Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = JewelTheme.typography.h2TextStyle) + IconButton({ adding = true }) { Icon(CirclePlus, "Add ${tab.addText}") Spacer(Modifier.width(5.dp)) Text("Add ${tab.addText}") diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt deleted file mode 100644 index eb1ef9c..0000000 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt +++ /dev/null @@ -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(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, available: List, -// selected: Int, onSelect: (Int) -> Unit, -// onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit, -// onImport: (List) -> 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, -// 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, -// 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, () -> 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, -// onImport: (List) -> 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()) } -// -// 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() -// } -// } -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsGradingView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsGradingView.kt index 616281b..7418004 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsGradingView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsGradingView.kt @@ -1,36 +1,13 @@ package com.jaytux.grader.ui import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.PrimaryScrollableTabRow -import androidx.compose.material3.ProvideTextStyle +//import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,16 +17,13 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.jaytux.grader.GroupGrading import com.jaytux.grader.app -import com.jaytux.grader.data.v2.CategoricGrade -import com.jaytux.grader.data.v2.Criterion -import com.jaytux.grader.data.v2.GradeType import com.jaytux.grader.data.v2.Group -import com.jaytux.grader.data.v2.Student -import com.jaytux.grader.viewmodel.Grade import com.jaytux.grader.viewmodel.GroupsGradingVM import com.jaytux.grader.viewmodel.Navigator -import org.jetbrains.exposed.v1.jdbc.transactions.transaction -import java.util.UUID +import java.util.* +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun GroupsGradingTitle(data: GroupGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Group Assignments / ${data.assignment.name} / Grading") @@ -65,7 +39,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) { val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) } Column(Modifier.padding(10.dp)) { - Text("Grading ${vm.base.name}", style = MaterialTheme.typography.headlineMedium) + Text("Grading ${vm.base.name}", style = JewelTheme.typography.h2TextStyle) Text("Group assignment in ${vm.course.name} - ${vm.edition.name}") Spacer(Modifier.height(5.dp)) Row(Modifier.fillMaxSize()) { @@ -87,7 +61,7 @@ fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) { Icon(DoubleBack, "Previous group") } Spacer(Modifier.width(10.dp)) - Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall) + Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle) Spacer(Modifier.weight(1f)) IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { Icon(DoubleForward, "Next group") @@ -154,15 +128,15 @@ fun GFWidget( Spacer(Modifier.width(5.dp)) Column(Modifier.align(Alignment.CenterVertically)) { Row { - Text(overrideName ?: crit.criterion.name, style = MaterialTheme.typography.bodyLarge) + Text(overrideName ?: crit.criterion.name, style = JewelTheme.typography.h4TextStyle) Spacer(Modifier.width(5.dp)) feedback.groupLevel?.grade?.let { Row(Modifier.align(Alignment.Bottom)) { - ProvideTextStyle(MaterialTheme.typography.bodySmall) { +// ProvideTextStyle(JewelTheme.typography.small) { Text("(Grade: ") it.render() Text(")") - } +// } } } } @@ -183,7 +157,7 @@ fun GFWidget( Spacer(Modifier.height(5.dp)) OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.height(5.dp)) - Button({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { + DefaultButton({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { Text("Save grade and feedback") } } @@ -193,7 +167,7 @@ fun GFWidget( Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), tonalElevation = 10.dp, shape = MaterialTheme.shapes.small) { Column(Modifier.padding(10.dp)) { - Text("Individual overrides", style = MaterialTheme.typography.bodyLarge) + Text("Individual overrides", style = JewelTheme.typography.h4TextStyle) feedback.overrides.forEach { (student, it) -> var enable by remember(key, it) { mutableStateOf(it != null) } var maybeRemoving by remember(key, it) { mutableStateOf(false) } @@ -207,7 +181,7 @@ fun GFWidget( Text(student.name, Modifier.align(Alignment.CenterVertically)) if(student.id.value in markOverridden) { Spacer(Modifier.width(5.dp)) - Text("(Overridden)", Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.bodySmall, fontStyle = FontStyle.Italic, color = Color.Red) + Text("(Overridden)", Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.small, fontStyle = FontStyle.Italic, color = Color.Red) } } @@ -220,7 +194,7 @@ fun GFWidget( Spacer(Modifier.height(5.dp)) OutlinedTextField(sText, { sText = it }, label = { Text("Feedback") }, singleLine = true, modifier = Modifier.fillMaxWidth()) Spacer(Modifier.height(5.dp)) - Button({ vm.modOverrideFeedback(crit.criterion, gr, student, groupLevel, sGrade, sText) }) { + DefaultButton({ vm.modOverrideFeedback(crit.criterion, gr, student, groupLevel, sGrade, sText) }) { Text("Save override") } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt index 19143d8..286548f 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt @@ -19,12 +19,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -55,6 +51,10 @@ import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.StringSelection import java.awt.datatransfer.Transferable import java.util.UUID +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalTextStyle +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { @@ -81,7 +81,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { else { Column(Modifier.padding(10.dp)) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { - Text(group.group.name, style = MaterialTheme.typography.headlineMedium) + Text(group.group.name, style = JewelTheme.typography.h2TextStyle) if (group.members.any { it.first.contact.isNotBlank() }) { IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) { Icon(Mail, "Send email", Modifier.fillMaxHeight()) @@ -106,7 +106,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { item { Surface(tonalElevation = 15.dp) { Row(Modifier.fillMaxWidth().padding(10.dp)) { - Text("Members", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp)) + Text("Members", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp)) } } } @@ -122,7 +122,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { if(role != null) { Surface(Modifier.align(Alignment.CenterVertically), tonalElevation = 5.dp, shape = MaterialTheme.shapes.small) { Box(Modifier.clickable { swappingRole = -1 }.clickable { swappingRole = idx }) { - Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = MaterialTheme.typography.labelMedium) + Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = JewelTheme.typography.regular) } } } @@ -148,7 +148,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { Spacer(Modifier.height(10.dp)) Column(Modifier.weight(0.5f)) { - Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall) + Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle) Surface(shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) { LazyColumn(Modifier.fillMaxHeight()) { item { @@ -190,7 +190,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { item { Surface(tonalElevation = 15.dp) { Row(Modifier.fillMaxWidth().padding(10.dp)) { - Text("Available Students", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp)) + Text("Available Students", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.padding(10.dp)) } } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/HomeView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/HomeView.kt index d903960..6b17745 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/HomeView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/HomeView.kt @@ -4,7 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,6 +15,9 @@ import com.jaytux.grader.EditionDetail import com.jaytux.grader.data.v2.Edition import com.jaytux.grader.viewmodel.HomeVM import com.jaytux.grader.viewmodel.Navigator +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun HomeTitle() = Text("Grader") @@ -27,8 +31,8 @@ fun HomeView(token: Navigator.NavToken) { LazyColumn(Modifier.padding(15.dp)) { item { Row { - Text("Courses Overview", Modifier.weight(0.8f), style = MaterialTheme.typography.headlineMedium) - Button({ addingCourse = true }) { + Text("Courses Overview", Modifier.weight(0.8f), style = JewelTheme.typography.h2TextStyle) + DefaultButton({ addingCourse = true }) { Icon(CirclePlus, "Add course") Spacer(Modifier.width(5.dp)) Text("Add course") @@ -48,7 +52,6 @@ fun HomeView(token: Navigator.NavToken) { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) { var addingEdition by remember { mutableStateOf(false) } @@ -56,13 +59,13 @@ fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) - Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.fillMaxWidth().padding(10.dp)) { Column(Modifier.padding(8.dp)) { Row { - Text(course.course.name, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f)) + Text(course.course.name, style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f)) IconButton({ deleting = true }) { Icon(Delete, "Delete course") } } Row { - Text("Editions", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f)) - Button({ addingEdition = true }) { + Text("Editions", style = JewelTheme.typography.h2TextStyle, modifier = Modifier.weight(1f)) + DefaultButton({ addingEdition = true }) { Icon(CirclePlus, "Add edition") Spacer(Modifier.width(5.dp)) Text("Add edition") @@ -74,7 +77,7 @@ fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) - } if(course.archived.isNotEmpty()) { - Text("Archived editions", style = MaterialTheme.typography.headlineSmall) + Text("Archived editions", style = JewelTheme.typography.h2TextStyle) FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) { course.archived.forEach { EditionCard(course.course.name, it, vm, onOpenEdition) } } @@ -103,31 +106,31 @@ fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onO Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.padding(10.dp).clickable { onOpen(edition.edition) }) { Column(Modifier.padding(10.dp).width(IntrinsicSize.Min)) { Column(Modifier.width(IntrinsicSize.Max)) { - Text(edition.edition.name, style = MaterialTheme.typography.headlineSmall) + Text(edition.edition.name, style = JewelTheme.typography.h2TextStyle) Text( "$type\n${edition.students.size} student(s) • ${edition.groups.size} group(s) • ${edition.assignments.size} assignment(s)", - style = MaterialTheme.typography.bodyMedium + style = JewelTheme.typography.regular ) } Spacer(Modifier.height(5.dp)) Row { if(edition.edition.archived) { - Button({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) { + DefaultButton({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) { Icon(Unarchive, "Unarchive edition") Spacer(Modifier.width(5.dp)) Text("Unarchive edition") } } else { - Button({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) { + DefaultButton({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) { Icon(Archive, "Archive edition") Spacer(Modifier.width(5.dp)) Text("Archive edition") } } Spacer(Modifier.width(10.dp)) - Button({ deleting = true }, Modifier.weight(0.5f)) { + DefaultButton({ deleting = true }, Modifier.weight(0.5f)) { Icon(Delete, "Archive edition") Spacer(Modifier.width(5.dp)) Text("Delete edition") diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/PeerEvalsGradingView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/PeerEvalsGradingView.kt index a8dd858..c2939cc 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/PeerEvalsGradingView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/PeerEvalsGradingView.kt @@ -1,36 +1,14 @@ package com.jaytux.grader.ui -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ScrollableTabRow -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Surface import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin @@ -42,19 +20,18 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.jaytux.grader.GroupGrading import com.jaytux.grader.PeerEvalGrading import com.jaytux.grader.app -import com.jaytux.grader.data.v2.CategoricGrade -import com.jaytux.grader.data.v2.GradeType import com.jaytux.grader.data.v2.Group -import com.jaytux.grader.data.v2.NumericGrade import com.jaytux.grader.data.v2.Student import com.jaytux.grader.viewmodel.Grade -import com.jaytux.grader.viewmodel.GroupsGradingVM import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.PeerEvalsGradingVM -import sun.tools.jconsole.LabeledComponent.layout +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalTextStyle +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.theme.colorPalette +import org.jetbrains.jewel.ui.typography @Composable fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading") @@ -77,7 +54,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) { } Column(Modifier.padding(10.dp)) { - Text("Grading ${vm.base.name}", style = MaterialTheme.typography.headlineMedium) + Text("Grading ${vm.base.name}", style = JewelTheme.typography.h2TextStyle) Text("Group assignment in ${vm.course.name} - ${vm.edition.name}") Spacer(Modifier.height(5.dp)) Row(Modifier.fillMaxSize()) { @@ -99,7 +76,7 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) { Icon(DoubleBack, "Previous group") } Spacer(Modifier.width(10.dp)) - Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall) + Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = JewelTheme.typography.h2TextStyle) Spacer(Modifier.weight(1f)) IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) { Icon(DoubleForward, "Next group") @@ -261,13 +238,13 @@ Column(Modifier.padding(10.dp).fillMaxHeight()) { var grade by remember(evaluator, evaluatee, current) { mutableStateOf(gradeState(critData, current?.grade)) } var text by remember(evaluator, evaluatee, current) { mutableStateOf(current?.feedback ?: "") } - Text(evaluatee, style = MaterialTheme.typography.headlineSmall) - Text("Evaluated by $evaluator", style = MaterialTheme.typography.bodyMedium, fontStyle = FontStyle.Italic) + Text(evaluatee, style = JewelTheme.typography.h2TextStyle) + Text("Evaluated by $evaluator", style = JewelTheme.typography.regular, fontStyle = FontStyle.Italic) Spacer(Modifier.height(10.dp)) GradePicker(grade, key = evaluator to evaluatee to current) { grade = it } OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 10, modifier = Modifier.fillMaxWidth()) Spacer(Modifier.height(10.dp)) - Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { + DefaultButton({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { Text("Save") } } @@ -281,7 +258,7 @@ fun SingleStudentGrade(name: String, current: FeedbackItem?, critData: CritData, Spacer(Modifier.height(5.dp)) OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.height(5.dp)) - Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { + DefaultButton({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) { Text("Save grade and feedback") } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt index 0a5ef5d..0962429 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt @@ -4,7 +4,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -12,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontStyle @@ -25,6 +25,8 @@ import com.jaytux.grader.toClipboard import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor import kotlinx.coroutines.launch +import org.jetbrains.jewel.foundation.theme.LocalContentColor +import org.jetbrains.jewel.ui.component.* @Composable fun RichTextStyleRow( @@ -211,13 +213,13 @@ fun RichTextStyleButton( // (Happens only on Desktop) .focusProperties { canFocus = false }, onClick = onClick, - colors = IconButtonDefaults.iconButtonColors( - contentColor = if (isSelected) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - }, - ), +// colors = IconButtonDefaults.iconButtonColors( +// contentColor = if (isSelected) { +// MaterialTheme.colorScheme.onPrimary +// } else { +// MaterialTheme.colorScheme.onBackground +// }, +// ), ) { Icon( icon, diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/SolosGradingView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/SolosGradingView.kt index 42b185a..24eb284 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/SolosGradingView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/SolosGradingView.kt @@ -1,6 +1,5 @@ package com.jaytux.grader.ui -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import com.jaytux.grader.GroupGrading @@ -8,6 +7,7 @@ import com.jaytux.grader.SoloGrading import com.jaytux.grader.viewmodel.GroupsGradingVM import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.SolosGradingVM +import org.jetbrains.jewel.ui.component.Text @Composable fun SolosGradingTitle(data: SoloGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Individual Assignments / ${data.assignment.name} / Grading") diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt index 33287e2..dc88c0a 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt @@ -16,15 +16,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,11 +30,14 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.jaytux.grader.data.v2.Edition import com.jaytux.grader.data.v2.Student import com.jaytux.grader.startEmail import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.SnackVM +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalTextStyle +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { @@ -69,7 +65,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) { Column(Modifier.padding(10.dp)) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { - Text(students[focus].name, style = MaterialTheme.typography.headlineSmall) + Text(students[focus].name, style = JewelTheme.typography.h2TextStyle) if(students[focus].contact.isNotBlank()) { IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) { Icon(Mail, "Send email", Modifier.fillMaxHeight()) @@ -109,7 +105,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { } Column { - Text("Groups:", style = MaterialTheme.typography.headlineSmall) + Text("Groups:", style = JewelTheme.typography.h2TextStyle) groups?.let { gList -> if(gList.isEmpty()) null else { @@ -142,7 +138,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { if(mod != students[focus].note) { Row { Spacer(Modifier.weight(1f)) - Button({ vm.modStudent(students[focus], null, null, mod) }) { + DefaultButton({ vm.modStudent(students[focus], null, null, mod) }) { Text("Update note") } } @@ -150,7 +146,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { } Spacer(Modifier.width(10.dp)) Column(Modifier.weight(0.66f)) { - Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall) + Text("Grade Summary: ", style = JewelTheme.typography.h2TextStyle) Surface(shape = MaterialTheme.shapes.medium, color = Color.White) { LazyColumn { item { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt deleted file mode 100644 index a2f935b..0000000 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt +++ /dev/null @@ -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 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, 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() -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index bf03a52..77142ac 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -7,7 +7,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.* +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,16 +32,18 @@ import androidx.compose.ui.window.* import com.jaytux.grader.maxN import com.jaytux.grader.viewmodel.Grade import kotlinx.datetime.* -import kotlinx.datetime.TimeZone import java.util.* -import kotlin.time.toJavaInstant +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.Outline +import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.typography @Composable fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) { Row { - Button({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) } + DefaultButton({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) } Spacer(Modifier.weight(0.1f)) - Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) } + DefaultButton({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) } } } @@ -224,7 +232,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on } Row { Spacer(Modifier.weight(1f)) - Text(grade.value.option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall) + Text(grade.value.option, fontStyle = FontStyle.Italic, style = JewelTheme.typography.small) } } } @@ -240,7 +248,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on ) Row { Spacer(Modifier.weight(1f)) - Text(grade.options[slider].option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall) + Text(grade.options[slider].option, fontStyle = FontStyle.Italic, style = JewelTheme.typography.small) } } } @@ -250,7 +258,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on var text by remember(grade, key) { mutableStateOf(grade.text) } OutlinedTextField(grade.text, { onUpdate(grade.copy(text = it)) }, Modifier.weight(1f), singleLine = true) - Button({ onUpdate(Grade.FreeText(text)) }, enabled = text != grade.text) { + DefaultButton({ onUpdate(Grade.FreeText(text)) }, enabled = text != grade.text) { Text("Save") } } @@ -261,7 +269,7 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on num, { num = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } }, Modifier.weight(1f), singleLine = true, isError = (num.toDoubleOrNull() ?: 0.0) > grade.grade.max ) - Button({ onUpdate(Grade.Numeric(num.toDoubleOrNull() ?: 0.0, grade.grade)) }, enabled = (num.toDoubleOrNull() ?: 0.0) <= grade.grade.max) { + DefaultButton({ onUpdate(Grade.Numeric(num.toDoubleOrNull() ?: 0.0, grade.grade)) }, enabled = (num.toDoubleOrNull() ?: 0.0) <= grade.grade.max) { Text("Save") } } @@ -269,9 +277,40 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on var perc by remember(grade, key) { mutableStateOf(grade.percentage.toString()) } OutlinedTextField("$perc%", { perc = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } }, Modifier.weight(1f), singleLine = true) - Button({ onUpdate(Grade.Percentage(perc.toDoubleOrNull() ?: 0.0)) }) { + DefaultButton({ onUpdate(Grade.Percentage(perc.toDoubleOrNull() ?: 0.0)) }) { Text("Save") } } } +} + +// TextField(true, name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text(label) }, isError = name in taken) + +@Composable +fun OutlinedTextField(value: String, onChange: (String) -> Unit, modifier: Modifier = Modifier, label: @Composable () -> Unit = {}, isError: Boolean = false, singleLine: Boolean = false, minLines: Int = 1) { + val state = remember { TextFieldState(value) } + + LaunchedEffect(value) { + if (state.text.toString() != value) { + state.edit { + replace(0, length, value) + } + } + } + + LaunchedEffect(state) { + snapshotFlow { state.text }.collect { newText -> + val newString = newText.toString() + if (newString != value) { + onChange(newString) + } + } + } + + if(singleLine) { + TextField(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label) + } + else { + TextArea(state, modifier, outline = if(isError) Outline.Error else Outline.None, placeholder = label /*, minLines = minLines*/) + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0a9bf2..e86af6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ rtf = "1.0.0-rc11" filekit = "0.10.0-beta04" directories = "26" androidx-activity-compose = "1.12.2" +jewel = "0.34.0-253.31033.149" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -41,6 +42,7 @@ filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", vers filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" } filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" } directories = { group = "dev.dirs", name = "directories", version.ref = "directories" } +jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone", version.ref = "jewel" } [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }