Compare commits
2 Commits
main
...
28c3b29c3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
28c3b29c3a
|
|||
|
18a7a82c36
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +26,15 @@ data class PeerEvalGrading(val course: Course, val edition: Edition, val assignm
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
MaterialTheme {
|
IntUiTheme(isDark = true) {
|
||||||
Navigator.NavHost(Home) {
|
Surface {
|
||||||
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
|
Navigator.NavHost(Home) {
|
||||||
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }
|
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
|
||||||
composable<GroupGrading>({ GroupsGradingTitle(it) }) { data, token -> GroupsGradingView(data, token) }
|
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }
|
||||||
composable<SoloGrading>({ SolosGradingTitle(it) }) { data, token -> SolosGradingView(data, token) }
|
composable<GroupGrading>({ GroupsGradingTitle(it) }) { data, token -> GroupsGradingView(data, token) }
|
||||||
composable<PeerEvalGrading>({ PeerEvalsGradingTitle(it) }) { data, token -> PeerEvalsGradingView(data, token) }
|
composable<SoloGrading>({ SolosGradingTitle(it) }) { data, token -> SolosGradingView(data, token) }
|
||||||
|
composable<PeerEvalGrading>({ PeerEvalsGradingTitle(it) }) { data, token -> PeerEvalsGradingView(data, token) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
|
||||||
@@ -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]
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user