Compare commits

..

10 Commits

12 changed files with 337 additions and 142 deletions

View File

@ -31,10 +31,15 @@ kotlin {
implementation(libs.exposed.core) implementation(libs.exposed.core)
implementation(libs.exposed.jdbc) implementation(libs.exposed.jdbc)
implementation(libs.exposed.dao) implementation(libs.exposed.dao)
implementation(libs.exposed.migration)
implementation(libs.exposed.kotlin.datetime) implementation(libs.exposed.kotlin.datetime)
implementation(libs.sqlite) implementation(libs.sqlite)
implementation(libs.material3.desktop) implementation(libs.material3.desktop)
implementation(libs.rtfield) implementation(libs.rtfield)
implementation(libs.filekit.core)
implementation(libs.filekit.dialogs)
implementation(libs.filekit.dialogs.compose)
implementation(libs.filekit.coil)
} }
} }
} }
@ -49,6 +54,10 @@ compose.desktop {
packageName = "com.jaytux.grader" packageName = "com.jaytux.grader"
packageVersion = "1.0.0" packageVersion = "1.0.0"
includeAllModules = true includeAllModules = true
linux {
modules("jdk.security.auth")
}
} }
} }
} }

View File

@ -56,6 +56,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
val name = varchar("name", 50) val name = varchar("name", 50)
val assignment = text("assignment") val assignment = text("assignment")
val deadline = datetime("deadline") val deadline = datetime("deadline")
val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id)
} }
object GroupAssignmentCriteria : UUIDTable("grpAsCr") { object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
@ -70,6 +71,7 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
val name = varchar("name", 50) val name = varchar("name", 50)
val assignment = text("assignment") val assignment = text("assignment")
val deadline = datetime("deadline") val deadline = datetime("deadline")
val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id)
} }
object SoloAssignmentCriteria : UUIDTable("soloAsCr") { object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
@ -86,7 +88,7 @@ object PeerEvaluations : UUIDTable("peerEvals") {
object GroupFeedbacks : CompositeIdTable("grpFdbks") { object GroupFeedbacks : CompositeIdTable("grpFdbks") {
val assignmentId = reference("group_assignment_id", GroupAssignments.id) val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
val groupId = reference("group_id", Groups.id) val groupId = reference("group_id", Groups.id)
val feedback = text("feedback") val feedback = text("feedback")
val grade = varchar("grade", 32) val grade = varchar("grade", 32)
@ -96,7 +98,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") {
object IndividualFeedbacks : CompositeIdTable("indivFdbks") { object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
val assignmentId = reference("group_assignment_id", GroupAssignments.id) val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
val groupId = reference("group_id", Groups.id) val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id) val studentId = reference("student_id", Students.id)
val feedback = text("feedback") val feedback = text("feedback")
@ -107,7 +109,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
object SoloFeedbacks : CompositeIdTable("soloFdbks") { object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val assignmentId = reference("solo_assignment_id", SoloAssignments.id) val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
val criterionId = reference("criterion_id", SoloAssignmentCriteria.id).nullable() val criterionId = reference("criterion_id", SoloAssignmentCriteria.id)
val studentId = reference("student_id", Students.id) val studentId = reference("student_id", Students.id)
val feedback = text("feedback") val feedback = text("feedback")
val grade = varchar("grade", 32) val grade = varchar("grade", 32)

View File

@ -1,8 +1,12 @@
package com.jaytux.grader.data package com.jaytux.grader.data
import MigrationUtils
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Database { object Database {
val db by lazy { val db by lazy {
@ -17,15 +21,19 @@ object Database {
StudentToGroupEvaluation StudentToGroupEvaluation
) )
val addMissing = SchemaUtils.addMissingColumnsStatements( val migrate = MigrationUtils.statementsRequiredForDatabaseMigration(
Courses, Editions, Groups, Courses, Editions, Groups,
Students, GroupStudents, EditionStudents, Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria, GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks, GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation, PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
StudentToGroupEvaluation StudentToGroupEvaluation,
withLogs = true
) )
addMissing.forEach { exec(it) }
println(" --- Migration --- ")
migrate.forEach { println(it); exec(it) }
println(" --- End migration --- ")
} }
actual actual
} }

View File

@ -60,6 +60,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by GroupAssignments.name var name by GroupAssignments.name
var assignment by GroupAssignments.assignment var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline var deadline by GroupAssignments.deadline
var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
} }
@ -98,6 +99,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by SoloAssignments.name var name by SoloAssignments.name
var assignment by SoloAssignments.assignment var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline var deadline by SoloAssignments.deadline
var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
} }

View File

@ -0,0 +1,90 @@
package com.jaytux.grader.data
import com.jaytux.grader.viewmodel.Assignment
import com.jaytux.grader.viewmodel.GroupAssignmentState
import io.github.vinceglb.filekit.PlatformFile
class MdBuilder {
private val content = StringBuilder()
fun appendHeader(text: String, level: Int = 1) {
require(level in 1..6) { "Header level must be between 1 and 6" }
content.appendLine()
content.appendLine("#".repeat(level) + " $text")
content.appendLine()
}
fun appendMd(text: String) { content.appendLine(text) }
fun appendParagraph(text: String, bold: Boolean = false, italic: Boolean = false) {
val formattedText = buildString {
if (bold) append("**")
if (italic) append("_")
append(text)
if (italic) append("_")
if (bold) append("**")
}
content.appendLine(formattedText)
content.appendLine()
}
fun build(): String = content.toString()
}
fun GroupAssignmentState.LocalGFeedback.exportTo(path: PlatformFile, assignment: GroupAssignment) {
val builder = MdBuilder()
builder.appendHeader("${assignment.name} Feedback for ${group.name}")
if(feedback.global != null && feedback.global.grade.isNotBlank()) {
val global = feedback.global.grade
builder.appendParagraph("Overall grade: ${feedback.global.grade}", true, true)
individuals.forEach { (student, it) ->
val (_, data) = it
if(data.global != null && data.global.grade.isNotBlank() && data.global.grade != global) {
builder.appendParagraph("${student.name} grade: ${data.global.grade}", true, true)
}
}
}
fun appendFeedback(heading: String, group: GroupAssignmentState.FeedbackEntry?, byStudent: List<Pair<Student, GroupAssignmentState.FeedbackEntry>>) {
if(group != null || byStudent.isNotEmpty()) {
builder.appendHeader(heading, 2)
if(group != null) {
if(group.grade.isNotBlank()) {
builder.appendParagraph("Group grade: ${group.grade}", true, true)
}
if(group.feedback.isNotBlank()) {
builder.appendMd(group.feedback)
}
}
byStudent.forEach { (student, it) ->
if(it.grade.isNotBlank() || it.feedback.isNotBlank()) builder.appendHeader(student.name, 3)
if(it.grade.isNotBlank()) {
builder.appendParagraph("Grade: ${it.grade}", true, true)
}
if(it.feedback.isNotBlank()) {
builder.appendMd(it.feedback)
}
}
}
}
appendFeedback("Overall Feedback", feedback.global,
individuals.mapNotNull { it.second.second.global?.let { g -> it.first to g } }
)
val criteria = (feedback.byCriterion.map { (c, _) -> c } +
individuals.flatMap { (_, it) -> it.second.byCriterion.map { (c, _) -> c } }).distinctBy { it.id.value }
criteria.forEach { c ->
appendFeedback(
c.name,
feedback.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry,
individuals.mapNotNull { (student, it) ->
val entry = it.second.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry
entry?.let { student to it }
}
)
}
path.file.writeText(builder.build())
}

View File

@ -4,10 +4,13 @@ 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.App
import com.jaytux.grader.data.Database import com.jaytux.grader.data.Database
import io.github.vinceglb.filekit.FileKit
fun main(){ fun main(){
Database.init() Database.init()
application { application {
FileKit.init(appId = "com.jaytux.grader")
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
title = "Grader", title = "Grader",

View File

@ -12,21 +12,22 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.text.font.FontStyle 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.text.input.TextFieldValue
import androidx.compose.ui.text.rememberTextMeasurer 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 com.jaytux.grader.data.GroupAssignment
import com.jaytux.grader.data.GroupAssignmentCriterion import com.jaytux.grader.data.GroupAssignmentCriterion
import com.jaytux.grader.data.SoloAssignmentCriterion import com.jaytux.grader.data.SoloAssignmentCriterion
import com.jaytux.grader.data.Student import com.jaytux.grader.data.Student
import com.jaytux.grader.data.exportTo
import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.GroupAssignmentState import com.jaytux.grader.viewmodel.GroupAssignmentState
import com.jaytux.grader.viewmodel.PeerEvaluationState import com.jaytux.grader.viewmodel.PeerEvaluationState
import com.jaytux.grader.viewmodel.SoloAssignmentState import com.jaytux.grader.viewmodel.SoloAssignmentState
import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction
@Composable @Composable
fun GroupAssignmentView(state: GroupAssignmentState) { fun GroupAssignmentView(state: GroupAssignmentState) {
@ -126,14 +127,7 @@ fun groupTaskWidget(
Row { Row {
DateTimePicker(deadline, onSetDeadline) DateTimePicker(deadline, onSetDeadline)
} }
RichTextStyleRow(state = updTask) RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") }
OutlinedRichTextEditor(
state = updTask,
modifier = Modifier.fillMaxWidth().weight(1f),
singleLine = false,
minLines = 5,
label = { Text("Task") }
)
CancelSaveRow( CancelSaveRow(
true, true,
{ updTask.setMarkdown(taskMD) }, { updTask.setMarkdown(taskMD) },
@ -188,18 +182,45 @@ fun groupTaskWidget(
@Composable @Composable
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) { fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
val (group, feedback, individual) = fdbk val (group, feedback, individual) = fdbk
var idx by remember(fdbk) { mutableStateOf(0) } var studentIdx by remember(fdbk) { mutableStateOf(0) }
var critIdx by remember(fdbk) { mutableStateOf(0) } var critIdx by remember(fdbk) { mutableStateOf(0) }
val criteria by state.criteria.entities val criteria by state.criteria.entities
val suggestions by state.autofill.entities val suggestions by state.autofill.entities
val exporting by remember { mutableStateOf(false) }
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 {
scope.launch { fdbk.exportTo(it, state.assignment) }
}
}
val critGrade: (Int) -> String? = { crit: Int ->
when {
studentIdx == 0 && crit == 0 -> feedback.global?.grade?.ifBlank { null }
studentIdx == 0 && crit != 0 -> feedback.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
studentIdx != 0 && crit == 0 -> individual[studentIdx - 1].second.second.global?.grade?.ifBlank { null }
else -> individual[studentIdx - 1].second.second.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
}.also { println("Mapping criterion #${crit} to grade ${it}") }
}
Row { Row {
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) { Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) { Column(Modifier.padding(10.dp)) {
LazyColumn(Modifier.weight(1f)) {
item { item {
Surface( Surface(
Modifier.fillMaxWidth().clickable { idx = 0 }, Modifier.fillMaxWidth().clickable { studentIdx = 0 },
tonalElevation = if (idx == 0) 50.dp else 0.dp, tonalElevation = if (studentIdx == 0) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic) Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
@ -209,98 +230,114 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
itemsIndexed(individual.toList()) { i, (student, details) -> itemsIndexed(individual.toList()) { i, (student, details) ->
val (role, _) = details val (role, _) = details
Surface( Surface(
Modifier.fillMaxWidth().clickable { idx = i + 1 }, Modifier.fillMaxWidth().clickable { studentIdx = i + 1 },
tonalElevation = if (idx == i + 1) 50.dp else 0.dp, tonalElevation = if (studentIdx == i + 1) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp)) Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
} }
} }
} }
}
val updateGrade = { grade: String -> Button(
if(idx == 0) { { exporter.launch("${state.assignment.name} (${fdbk.group.name})", "md") },
state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade) Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
) {
Text("Export group feedback")
} }
else {
val ind = individual[idx - 1]
val glob = ind.second.second.global
state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade)
} }
} }
val updateFeedback = { fdbk: String -> Column(Modifier.weight(0.75f).padding(10.dp)) {
if(idx == 0) { TabRow(critIdx) {
if(critIdx == 0) { Tab(critIdx == 0, { critIdx = 0 }) {
state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null) Text(
"General feedback",
Modifier.padding(5.dp),
fontStyle = FontStyle.Italic
)
} }
else { criteria.forEachIndexed { i, c ->
val current = feedback.byCriterion[critIdx - 1] Tab(critIdx == i + 1, { critIdx = i + 1 }) {
state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion) Text(
} c.name,
} Modifier.padding(5.dp)
else { )
val ind = individual[idx - 1]
if(critIdx == 0) {
val entry = ind.second.second
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.global?.grade ?: "", null)
}
else {
val entry = ind.second.second.byCriterion[critIdx - 1]
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.entry?.grade ?: "", entry.criterion)
} }
} }
} }
Spacer(Modifier.height(5.dp))
groupFeedbackPane( groupFeedbackPane(
criteria, critIdx, { critIdx = it }, feedback.global, criteria, critIdx, { critIdx = it },
if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry, when {
suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp), studentIdx == 0 && critIdx == 0 -> feedback.global
key = idx to critIdx 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,
key = studentIdx to critIdx
) )
} }
} }
}
@Composable @Composable
fun groupFeedbackPane( fun groupFeedbackPane(
criteria: List<GroupAssignmentCriterion>, criteria: List<GroupAssignmentCriterion>,
currentCriterion: Int, currentCriterion: Int,
onSelectCriterion: (Int) -> Unit, onSelectCriterion: (Int) -> Unit,
globFeedback: GroupAssignmentState.FeedbackEntry?, rawFeedback: GroupAssignmentState.FeedbackEntry?,
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
autofill: List<String>, autofill: List<String>,
onSetGrade: (String) -> Unit, onSave: (String, String) -> Unit,
onSetFeedback: (String) -> Unit, critGrades: List<Pair<String, String?>>? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
key: Any? = null key: Any? = null
) { ) {
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } var grade by remember(rawFeedback, key) { mutableStateOf(rawFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } val feedback = rememberRichTextState()
LaunchedEffect(currentCriterion, criteria, rawFeedback, key) {
feedback.setMarkdown(rawFeedback?.feedback ?: "")
}
Column(modifier) { Column(modifier) {
Row { Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f)) Spacer(Modifier.weight(0.6f))
Button( Button(
{ onSetGrade(grade); onSetFeedback(feedback.text) }, { onSave(grade, feedback.toMarkdown()) },
Modifier.weight(0.2f).align(Alignment.CenterVertically), Modifier.weight(0.2f).align(Alignment.CenterVertically)
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
) { ) {
Text("Save") 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)) Spacer(Modifier.height(5.dp))
AutocompleteLineField( Row {
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } RichTextField(feedback, outerModifier = Modifier.weight(0.7f).fillMaxHeight()) { Text("Feedback") }
) { filter -> critGrades?.let { grades ->
autofill.filter { x -> x.trim().startsWith(filter.trim()) } Spacer(Modifier.width(10.dp))
LazyColumn(Modifier.weight(0.3f)) {
item {
Text("Criteria grades", Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
}
items(grades) { (crit, grade) ->
Column {
Text(crit, Modifier.padding(5.dp), fontWeight = FontWeight.Bold)
Row {
Spacer(Modifier.width(5.dp))
if(grade == null) Text("(no grade yet)", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
else Text(grade, Modifier.padding(5.dp))
}
Spacer(Modifier.width(10.dp))
}
}
}
}
} }
} }
} }
@ -480,16 +517,19 @@ fun soloFeedbackPane(
key: Any? = null key: Any? = null
) { ) {
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } val feedback = rememberRichTextState()
LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) {
feedback.setMarkdown(criterionFeedback?.feedback ?: "")
}
Column(modifier) { Column(modifier) {
Row { Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f)) Spacer(Modifier.weight(0.6f))
Button( Button(
{ onSetGrade(grade); onSetFeedback(feedback.text) }, { onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) },
Modifier.weight(0.2f).align(Alignment.CenterVertically), Modifier.weight(0.2f).align(Alignment.CenterVertically)
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
) { ) {
Text("Save") Text("Save")
} }
@ -501,11 +541,7 @@ fun soloFeedbackPane(
} }
} }
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
AutocompleteLineField( RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter ->
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
}
} }
} }

View File

@ -64,14 +64,14 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
{ name, note, contact, add -> state.newStudent(name, contact, note, add) }, { name, note, contact, add -> state.newStudent(name, contact, note, add) },
{ students -> state.addToCourse(students) }, { students -> state.addToCourse(students) },
{ s, name -> state.setStudentName(s, name) } { s, name -> state.setStudentName(s, name) }
) { s -> state.delete(s) } ) { s, idx -> state.delete(s); if(id == idx) state.clearHistoryIndex() }
OpenPanel.Group -> GroupPanel( OpenPanel.Group -> GroupPanel(
course, edition, groups, id, course, edition, groups, id,
{ state.navTo(it) }, { state.navTo(it) },
{ name -> state.newGroup(name) }, { name -> state.newGroup(name) },
{ g, name -> state.setGroupName(g, name) } { g, name -> state.setGroupName(g, name) }
) { g -> state.delete(g) } ) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
OpenPanel.Assignment -> AssignmentPanel( OpenPanel.Assignment -> AssignmentPanel(
course, edition, mergedAssignments, id, course, edition, mergedAssignments, id,
@ -79,7 +79,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
{ type, name -> state.newAssignment(type, name) }, { type, name -> state.newAssignment(type, name) },
{ a, name -> state.setAssignmentTitle(a, name) }, { a, name -> state.setAssignmentTitle(a, name) },
{ a1, a2 -> state.swapOrder(a1, a2) } { a1, a2 -> state.swapOrder(a1, a2) }
) { a -> state.delete(a) } ) { a, idx -> state.delete(a); if(id == idx) state.clearHistoryIndex() }
} }
} }
} }
@ -133,7 +133,7 @@ fun StudentPanel(
course: Course, edition: Edition, students: List<Student>, available: List<Student>, course: Course, edition: Edition, students: List<Student>, available: List<Student>,
selected: Int, onSelect: (Int) -> Unit, selected: Int, onSelect: (Int) -> Unit,
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit, onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student) -> Unit onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student, Int) -> Unit
) = Column(Modifier.padding(10.dp)) { ) = Column(Modifier.padding(10.dp)) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(-1) } var deleting by remember { mutableStateOf(-1) }
@ -171,7 +171,7 @@ fun StudentPanel(
ConfirmDeleteDialog( ConfirmDeleteDialog(
"a student", "a student",
{ deleting = -1 }, { deleting = -1 },
{ onDelete(students[deleting]) } { onDelete(students[deleting], deleting) }
) { Text(students[deleting].name) } ) { Text(students[deleting].name) }
} }
} }
@ -180,7 +180,7 @@ fun StudentPanel(
fun GroupPanel( fun GroupPanel(
course: Course, edition: Edition, groups: List<Group>, course: Course, edition: Edition, groups: List<Group>,
selected: Int, onSelect: (Int) -> Unit, selected: Int, onSelect: (Int) -> Unit,
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group) -> Unit onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group, Int) -> Unit
) = Column(Modifier.padding(10.dp)) { ) = Column(Modifier.padding(10.dp)) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(-1) } var deleting by remember { mutableStateOf(-1) }
@ -218,7 +218,7 @@ fun GroupPanel(
ConfirmDeleteDialog( ConfirmDeleteDialog(
"a group", "a group",
{ deleting = -1 }, { deleting = -1 },
{ onDelete(groups[deleting]) } { onDelete(groups[deleting], deleting) }
) { Text(groups[deleting].name) } ) { Text(groups[deleting].name) }
} }
} }
@ -228,7 +228,7 @@ fun AssignmentPanel(
course: Course, edition: Edition, assignments: List<Assignment>, course: Course, edition: Edition, assignments: List<Assignment>,
selected: Int, onSelect: (Int) -> Unit, selected: Int, onSelect: (Int) -> Unit,
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit, onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment, Int) -> Unit
) = Column(Modifier.padding(10.dp)) { ) = Column(Modifier.padding(10.dp)) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(-1) } var deleting by remember { mutableStateOf(-1) }
@ -315,8 +315,8 @@ fun AssignmentPanel(
ConfirmDeleteDialog( ConfirmDeleteDialog(
"an assignment", "an assignment",
{ deleting = -1 }, { deleting = -1 },
{ onDelete(assignments[deleting]) } { onDelete(assignments[deleting], deleting) }
) { Text(assignments[deleting].name()) } ) { if(deleting != -1) Text(assignments[deleting].name()) }
} }
} }

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
import com.jaytux.grader.loadClipboard import com.jaytux.grader.loadClipboard
import com.jaytux.grader.toClipboard import com.jaytux.grader.toClipboard
import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
@Composable @Composable
fun RichTextStyleRow( fun RichTextStyleRow(
@ -238,3 +239,17 @@ fun RichTextStyleButton(
) )
} }
} }
@Composable
fun RichTextField(
state: RichTextState,
modifier: Modifier = Modifier.fillMaxSize(),
buttonsModifier: Modifier = Modifier.fillMaxWidth(),
outerModifier: Modifier = Modifier,
label: @Composable (() -> Unit)? = null
) = Column(outerModifier) {
RichTextStyleRow(buttonsModifier, state)
OutlinedRichTextEditor(
state = state, modifier = modifier, singleLine = false, minLines = 5, label = label
)
}

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.data.Course import com.jaytux.grader.data.Course
import com.jaytux.grader.data.Edition import com.jaytux.grader.data.Edition
import com.jaytux.grader.viewmodel.PeerEvaluationState import com.jaytux.grader.viewmodel.PeerEvaluationState
import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.* import kotlinx.datetime.*
@ -211,7 +212,8 @@ fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun AutocompleteLineField( fun AutocompleteLineField__(
// state: RichTextState,
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
onFilter: (String) -> List<String> onFilter: (String) -> List<String>

View File

@ -171,10 +171,14 @@ class EditionState(val edition: Edition) {
fun newSoloAssignment(name: String) { fun newSoloAssignment(name: String) {
transaction { transaction {
SoloAssignment.new { val assign = SoloAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx() this.number = nextIdx()
} }
val global = SoloAssignmentCriterion.new {
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
}
assign.globalCriterion = global
solo.refresh() solo.refresh()
} }
} }
@ -186,10 +190,14 @@ class EditionState(val edition: Edition) {
} }
fun newGroupAssignment(name: String) { fun newGroupAssignment(name: String) {
transaction { transaction {
GroupAssignment.new { val assign = GroupAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx() this.number = nextIdx()
} }
val global = GroupAssignmentCriterion.new {
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
}
assign.globalCriterion = global
groupAs.refresh() groupAs.refresh()
} }
} }
@ -354,6 +362,10 @@ class EditionState(val edition: Edition) {
while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1) while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1)
_history.value = temp _history.value = temp
} }
fun clearHistoryIndex() {
val last = _history.value.lastOrNull() ?: return
_history.value = _history.value.filter { (i, panel) -> panel != last.second || i != last.first } + (-1 to last.second)
}
} }
class StudentState(val student: Student, edition: Edition) { class StudentState(val student: Student, edition: Edition) {
@ -373,12 +385,12 @@ class StudentState(val student: Student, edition: Edition) {
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and (GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
(GroupAssignmentCriteria.name eq "") (GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
}.map { it[GroupAssignments.id] to it } }.map { it[GroupAssignments.id] to it }
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
(IndividualFeedbacks.studentId eq student.id) and (IndividualFeedbacks.studentId eq student.id) and
(GroupAssignmentCriteria.name eq "") (GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
}.map { it[GroupAssignments.id] to it } }.map { it[GroupAssignments.id] to it }
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>() val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
@ -475,7 +487,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState { val criteria = RawDbState {
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList() assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
} }
val feedback = RawDbState { loadFeedback() } val feedback = RawDbState { loadFeedback() }
@ -494,7 +506,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> { private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
val allCrit = GroupAssignmentCriterion.find { val allCrit = GroupAssignmentCriterion.find {
GroupAssignmentCriteria.assignmentId eq assignment.id GroupAssignmentCriteria.assignmentId eq assignment.id
} }.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
return Group.find { return Group.find {
(Groups.editionId eq assignment.edition.id) (Groups.editionId eq assignment.edition.id)
@ -502,16 +514,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where { val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id) (GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
}.map { row -> }.map { row ->
val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] } val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]]
val fdbk = row[GroupFeedbacks.feedback] val fdbk = row[GroupFeedbacks.feedback]
val grade = row[GroupFeedbacks.grade] val grade = row[GroupFeedbacks.grade]
crit to FeedbackEntry(fdbk, grade) crit to FeedbackEntry(fdbk, grade)
} }
val global = forGroup.firstOrNull { it.first == null }?.second val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } val byCrit_ = forGroup
.filterNotNull().associateBy { it.criterion.id } .filter{ it.first.id != assignment.globalCriterion.id }
.map { LocalCriterionFeedback(it.first, it.second) }
.associateBy { it.criterion.id }
val byCrit = allCrit.map { c -> val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: LocalCriterionFeedback(c, null) byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
} }
@ -522,21 +537,25 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val student = it.student val student = it.student
val role = it.role val role = it.role
val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents) val forSt = (IndividualFeedbacks innerJoin Groups)
.selectAll().where { .selectAll().where {
(IndividualFeedbacks.assignmentId eq assignment.id) and (IndividualFeedbacks.assignmentId eq assignment.id) and
(GroupStudents.studentId eq student.id) and (Groups.id eq group.id) (IndividualFeedbacks.studentId eq student.id) and (Groups.id eq group.id)
}.map { row -> }.map { row ->
val crit = row[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] } val stdId = row[IndividualFeedbacks.studentId]
val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
val fdbk = row[IndividualFeedbacks.feedback] val fdbk = row[IndividualFeedbacks.feedback]
val grade = row[IndividualFeedbacks.grade] val grade = row[IndividualFeedbacks.grade]
crit to FeedbackEntry(fdbk, grade) crit to FeedbackEntry(fdbk, grade)
} }
val global = forSt.firstOrNull { it.first == null }?.second val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } val byCrit_ = forSt
.filterNotNull().associateBy { it.criterion.id } .filter { it.first != assignment.globalCriterion.id }
.map { LocalCriterionFeedback(it.first, it.second) }
.associateBy { it.criterion.id }
val byCrit = allCrit.map { c -> val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: LocalCriterionFeedback(c, null) byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
} }
@ -556,7 +575,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[groupId] = group.id it[groupId] = group.id
it[this.feedback] = msg it[this.feedback] = msg
it[this.grade] = grd it[this.grade] = grd
it[criterionId] = criterion?.id it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()
@ -570,7 +589,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[studentId] = student.id it[studentId] = student.id
it[this.feedback] = msg it[this.feedback] = msg
it[this.grade] = grd it[this.grade] = grd
it[criterionId] = criterion?.id it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()
@ -628,7 +647,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState { val criteria = RawDbState {
assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).toList() assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
} }
val feedback = RawDbState { loadFeedback() } val feedback = RawDbState { loadFeedback() }
@ -638,25 +657,28 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
}.flatten().distinct().sorted() }.flatten().distinct().sorted()
} }
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> { private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {3
val allCrit = SoloAssignmentCriterion.find { val allCrit = SoloAssignmentCriterion.find {
SoloAssignmentCriteria.assignmentId eq assignment.id SoloAssignmentCriteria.assignmentId eq assignment.id
} }.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student -> return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where { val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id) (IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
}.map { row -> }.map { row ->
val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] } val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
val fdbk = row[IndividualFeedbacks.feedback] val fdbk = row[IndividualFeedbacks.feedback]
val grade = row[IndividualFeedbacks.grade] val grade = row[IndividualFeedbacks.grade]
crit to LocalFeedback(fdbk, grade) crit to LocalFeedback(fdbk, grade)
} }
val global = forStudent.firstOrNull { it.first == null }?.second val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second
val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } } val byCrit_ = forStudent
.filterNotNull().associateBy { it.first.id } .filter { it.first != assignment.globalCriterion.id }
.map { Pair(it.first, it.second) }
.associateBy { it.first.id }
val byCrit = allCrit.map { c -> val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: Pair(c, null) byCrit_[c.id] ?: Pair(c, null)
} }
@ -672,7 +694,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
it[studentId] = student.id it[studentId] = student.id
it[this.feedback] = msg ?: "" it[this.feedback] = msg ?: ""
it[this.grade] = grd ?: "" it[this.grade] = grd ?: ""
it[criterionId] = criterion?.id it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()

View File

@ -1,6 +1,6 @@
[versions] [versions]
androidx-lifecycle = "2.8.4" androidx-lifecycle = "2.8.4"
compose-multiplatform = "1.7.0" compose-multiplatform = "1.8.1"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.1.0" kotlin = "2.1.0"
kotlinx-coroutines = "1.10.1" kotlinx-coroutines = "1.10.1"
@ -9,6 +9,7 @@ material3 = "1.7.3"
ui-android = "1.7.8" ui-android = "1.7.8"
foundation-layout-android = "1.7.8" foundation-layout-android = "1.7.8"
rtf = "1.0.0-rc11" rtf = "1.0.0-rc11"
filekit = "0.10.0-beta04"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -20,6 +21,7 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" } exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" } exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" }
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" } exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
exposed-migration = { group = "org.jetbrains.exposed", name = "exposed-migration", version.ref = "exposed" }
exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" } exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" }
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" } sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" } sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
@ -29,6 +31,10 @@ material-icons = { group = "org.jetbrains.compose.material", name = "material-ic
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" } androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" } androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" } rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", 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" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }