Compare commits
10 Commits
6b80457da5
...
eca161b251
Author | SHA1 | Date | |
---|---|---|---|
eca161b251
|
|||
0d6f361a45
|
|||
59d97f8ce5
|
|||
497af308fe | |||
119ff4b6c5
|
|||
0883d2332e
|
|||
f7b4f29e2e
|
|||
6dc88285d0
|
|||
29d23f8400
|
|||
3a47154969
|
@ -31,10 +31,15 @@ kotlin {
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.migration)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.material3.desktop)
|
||||
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"
|
||||
packageVersion = "1.0.0"
|
||||
includeAllModules = true
|
||||
|
||||
linux {
|
||||
modules("jdk.security.auth")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
|
||||
val name = varchar("name", 50)
|
||||
val assignment = text("assignment")
|
||||
val deadline = datetime("deadline")
|
||||
val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id)
|
||||
}
|
||||
|
||||
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
|
||||
@ -70,6 +71,7 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
|
||||
val name = varchar("name", 50)
|
||||
val assignment = text("assignment")
|
||||
val deadline = datetime("deadline")
|
||||
val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id)
|
||||
}
|
||||
|
||||
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
|
||||
@ -86,7 +88,7 @@ object PeerEvaluations : UUIDTable("peerEvals") {
|
||||
|
||||
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||
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 feedback = text("feedback")
|
||||
val grade = varchar("grade", 32)
|
||||
@ -96,7 +98,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||
|
||||
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
||||
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 studentId = reference("student_id", Students.id)
|
||||
val feedback = text("feedback")
|
||||
@ -107,7 +109,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
||||
|
||||
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
||||
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 feedback = text("feedback")
|
||||
val grade = varchar("grade", 32)
|
||||
|
@ -1,8 +1,12 @@
|
||||
package com.jaytux.grader.data
|
||||
|
||||
import MigrationUtils
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
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.update
|
||||
|
||||
object Database {
|
||||
val db by lazy {
|
||||
@ -17,15 +21,19 @@ object Database {
|
||||
StudentToGroupEvaluation
|
||||
)
|
||||
|
||||
val addMissing = SchemaUtils.addMissingColumnsStatements(
|
||||
val migrate = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
Courses, Editions, Groups,
|
||||
Students, GroupStudents, EditionStudents,
|
||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
||||
StudentToGroupEvaluation
|
||||
StudentToGroupEvaluation,
|
||||
withLogs = true
|
||||
)
|
||||
addMissing.forEach { exec(it) }
|
||||
|
||||
println(" --- Migration --- ")
|
||||
migrate.forEach { println(it); exec(it) }
|
||||
println(" --- End migration --- ")
|
||||
}
|
||||
actual
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
var name by GroupAssignments.name
|
||||
var assignment by GroupAssignments.assignment
|
||||
var deadline by GroupAssignments.deadline
|
||||
var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion
|
||||
|
||||
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 assignment by SoloAssignments.assignment
|
||||
var deadline by SoloAssignments.deadline
|
||||
var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion
|
||||
|
||||
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
@ -4,10 +4,13 @@ import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import com.jaytux.grader.App
|
||||
import com.jaytux.grader.data.Database
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
|
||||
fun main(){
|
||||
Database.init()
|
||||
application {
|
||||
FileKit.init(appId = "com.jaytux.grader")
|
||||
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Grader",
|
||||
|
@ -12,21 +12,22 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jaytux.grader.data.GroupAssignment
|
||||
import com.jaytux.grader.data.GroupAssignmentCriterion
|
||||
import com.jaytux.grader.data.SoloAssignmentCriterion
|
||||
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.PeerEvaluationState
|
||||
import com.jaytux.grader.viewmodel.SoloAssignmentState
|
||||
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction
|
||||
|
||||
@Composable
|
||||
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||
@ -126,14 +127,7 @@ fun groupTaskWidget(
|
||||
Row {
|
||||
DateTimePicker(deadline, onSetDeadline)
|
||||
}
|
||||
RichTextStyleRow(state = updTask)
|
||||
OutlinedRichTextEditor(
|
||||
state = updTask,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
singleLine = false,
|
||||
minLines = 5,
|
||||
label = { Text("Task") }
|
||||
)
|
||||
RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") }
|
||||
CancelSaveRow(
|
||||
true,
|
||||
{ updTask.setMarkdown(taskMD) },
|
||||
@ -188,77 +182,106 @@ fun groupTaskWidget(
|
||||
@Composable
|
||||
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
||||
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) }
|
||||
val criteria by state.criteria.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 {
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = 0 },
|
||||
tonalElevation = if (idx == 0) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(individual.toList()) { i, (student, details) ->
|
||||
val (role, _) = details
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
||||
tonalElevation = if (idx == 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") },
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val updateGrade = { grade: String ->
|
||||
if(idx == 0) {
|
||||
state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade)
|
||||
}
|
||||
else {
|
||||
val ind = individual[idx - 1]
|
||||
val glob = ind.second.second.global
|
||||
state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
|
||||
val updateFeedback = { fdbk: String ->
|
||||
if(idx == 0) {
|
||||
if(critIdx == 0) {
|
||||
state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null)
|
||||
}
|
||||
else {
|
||||
val current = feedback.byCriterion[critIdx - 1]
|
||||
state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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,
|
||||
key = studentIdx to critIdx
|
||||
)
|
||||
}
|
||||
|
||||
groupFeedbackPane(
|
||||
criteria, critIdx, { critIdx = it }, feedback.global,
|
||||
if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry,
|
||||
suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp),
|
||||
key = idx to critIdx
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,40 +290,54 @@ fun groupFeedbackPane(
|
||||
criteria: List<GroupAssignmentCriterion>,
|
||||
currentCriterion: Int,
|
||||
onSelectCriterion: (Int) -> Unit,
|
||||
globFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
rawFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
autofill: List<String>,
|
||||
onSetGrade: (String) -> Unit,
|
||||
onSetFeedback: (String) -> Unit,
|
||||
onSave: (String, String) -> Unit,
|
||||
critGrades: List<Pair<String, String?>>? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
key: Any? = null
|
||||
) {
|
||||
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
|
||||
var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
|
||||
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("Overall grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||
Spacer(Modifier.weight(0.6f))
|
||||
Button(
|
||||
{ onSetGrade(grade); onSetFeedback(feedback.text) },
|
||||
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
|
||||
{ onSave(grade, 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))
|
||||
AutocompleteLineField(
|
||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
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)
|
||||
}
|
||||
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
|
||||
) {
|
||||
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) {
|
||||
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.text) },
|
||||
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
|
||||
{ onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) },
|
||||
Modifier.weight(0.2f).align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
@ -501,11 +541,7 @@ fun soloFeedbackPane(
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
AutocompleteLineField(
|
||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
}
|
||||
RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,14 +64,14 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
||||
{ students -> state.addToCourse(students) },
|
||||
{ s, name -> state.setStudentName(s, name) }
|
||||
) { s -> state.delete(s) }
|
||||
) { 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 -> state.delete(g) }
|
||||
) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
|
||||
|
||||
OpenPanel.Assignment -> AssignmentPanel(
|
||||
course, edition, mergedAssignments, id,
|
||||
@ -79,7 +79,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||
{ type, name -> state.newAssignment(type, name) },
|
||||
{ a, name -> state.setAssignmentTitle(a, name) },
|
||||
{ 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>,
|
||||
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) -> 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) }
|
||||
@ -171,7 +171,7 @@ fun StudentPanel(
|
||||
ConfirmDeleteDialog(
|
||||
"a student",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(students[deleting]) }
|
||||
{ onDelete(students[deleting], deleting) }
|
||||
) { Text(students[deleting].name) }
|
||||
}
|
||||
}
|
||||
@ -180,7 +180,7 @@ fun StudentPanel(
|
||||
fun GroupPanel(
|
||||
course: Course, edition: Edition, groups: List<Group>,
|
||||
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)) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(-1) }
|
||||
@ -218,7 +218,7 @@ fun GroupPanel(
|
||||
ConfirmDeleteDialog(
|
||||
"a group",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(groups[deleting]) }
|
||||
{ onDelete(groups[deleting], deleting) }
|
||||
) { Text(groups[deleting].name) }
|
||||
}
|
||||
}
|
||||
@ -228,7 +228,7 @@ 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) -> 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) }
|
||||
@ -315,8 +315,8 @@ fun AssignmentPanel(
|
||||
ConfirmDeleteDialog(
|
||||
"an assignment",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(assignments[deleting]) }
|
||||
) { Text(assignments[deleting].name()) }
|
||||
{ onDelete(assignments[deleting], deleting) }
|
||||
) { if(deleting != -1) Text(assignments[deleting].name()) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.jaytux.grader.loadClipboard
|
||||
import com.jaytux.grader.toClipboard
|
||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
|
||||
|
||||
@Composable
|
||||
fun RichTextStyleRow(
|
||||
@ -237,4 +238,18 @@ 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
|
||||
)
|
||||
}
|
@ -34,6 +34,7 @@ import androidx.compose.ui.window.*
|
||||
import com.jaytux.grader.data.Course
|
||||
import com.jaytux.grader.data.Edition
|
||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.*
|
||||
@ -211,7 +212,8 @@ fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AutocompleteLineField(
|
||||
fun AutocompleteLineField__(
|
||||
// state: RichTextState,
|
||||
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
||||
onFilter: (String) -> List<String>
|
||||
|
@ -171,10 +171,14 @@ class EditionState(val edition: Edition) {
|
||||
|
||||
fun newSoloAssignment(name: String) {
|
||||
transaction {
|
||||
SoloAssignment.new {
|
||||
val assign = SoloAssignment.new {
|
||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -186,10 +190,14 @@ class EditionState(val edition: Edition) {
|
||||
}
|
||||
fun newGroupAssignment(name: String) {
|
||||
transaction {
|
||||
GroupAssignment.new {
|
||||
val assign = GroupAssignment.new {
|
||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -354,6 +362,10 @@ class EditionState(val edition: Edition) {
|
||||
while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1)
|
||||
_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) {
|
||||
@ -373,12 +385,12 @@ class StudentState(val student: Student, edition: Edition) {
|
||||
|
||||
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
|
||||
(GroupAssignmentCriteria.name eq "")
|
||||
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
||||
}.map { it[GroupAssignments.id] to it }
|
||||
|
||||
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
||||
(IndividualFeedbacks.studentId eq student.id) and
|
||||
(GroupAssignmentCriteria.name eq "")
|
||||
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
||||
}.map { it[GroupAssignments.id] to it }
|
||||
|
||||
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
|
||||
@ -396,7 +408,7 @@ class StudentState(val student: Student, edition: Edition) {
|
||||
val og = res[gAId] ?: LocalGroupGrade(iRow[Groups.name], iRow[GroupAssignments.name], null, null)
|
||||
res[gAId] = og.copy(indivGrade = iRow[IndividualFeedbacks.grade])
|
||||
}
|
||||
|
||||
|
||||
res.values.toList()
|
||||
}
|
||||
|
||||
@ -475,7 +487,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||
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() }
|
||||
|
||||
@ -494,7 +506,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
||||
val allCrit = GroupAssignmentCriterion.find {
|
||||
GroupAssignmentCriteria.assignmentId eq assignment.id
|
||||
}
|
||||
}.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
||||
|
||||
return Group.find {
|
||||
(Groups.editionId eq assignment.edition.id)
|
||||
@ -502,16 +514,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
|
||||
}.map { row ->
|
||||
val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] }
|
||||
val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]]
|
||||
val fdbk = row[GroupFeedbacks.feedback]
|
||||
val grade = row[GroupFeedbacks.grade]
|
||||
|
||||
crit to FeedbackEntry(fdbk, grade)
|
||||
}
|
||||
|
||||
val global = forGroup.firstOrNull { it.first == null }?.second
|
||||
val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
|
||||
.filterNotNull().associateBy { it.criterion.id }
|
||||
val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
||||
val byCrit_ = forGroup
|
||||
.filter{ it.first.id != assignment.globalCriterion.id }
|
||||
.map { LocalCriterionFeedback(it.first, it.second) }
|
||||
.associateBy { it.criterion.id }
|
||||
|
||||
val byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||
}
|
||||
@ -522,21 +537,25 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
val student = it.student
|
||||
val role = it.role
|
||||
|
||||
val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents)
|
||||
val forSt = (IndividualFeedbacks innerJoin Groups)
|
||||
.selectAll().where {
|
||||
(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 ->
|
||||
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 grade = row[IndividualFeedbacks.grade]
|
||||
|
||||
crit to FeedbackEntry(fdbk, grade)
|
||||
}
|
||||
|
||||
val global = forSt.firstOrNull { it.first == null }?.second
|
||||
val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
|
||||
.filterNotNull().associateBy { it.criterion.id }
|
||||
val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
||||
val byCrit_ = forSt
|
||||
.filter { it.first != assignment.globalCriterion.id }
|
||||
.map { LocalCriterionFeedback(it.first, it.second) }
|
||||
.associateBy { it.criterion.id }
|
||||
|
||||
val byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||
}
|
||||
@ -556,7 +575,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
it[groupId] = group.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
it[criterionId] = criterion?.id
|
||||
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||
}
|
||||
}
|
||||
feedback.refresh(); autofill.refresh()
|
||||
@ -570,7 +589,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
it[studentId] = student.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
it[criterionId] = criterion?.id
|
||||
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||
}
|
||||
}
|
||||
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 _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||
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() }
|
||||
|
||||
@ -638,25 +657,28 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
|
||||
}.flatten().distinct().sorted()
|
||||
}
|
||||
|
||||
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
|
||||
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {3
|
||||
val allCrit = SoloAssignmentCriterion.find {
|
||||
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 ->
|
||||
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
|
||||
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
|
||||
}.map { row ->
|
||||
val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] }
|
||||
val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
|
||||
val fdbk = row[IndividualFeedbacks.feedback]
|
||||
val grade = row[IndividualFeedbacks.grade]
|
||||
|
||||
crit to LocalFeedback(fdbk, grade)
|
||||
}
|
||||
|
||||
val global = forStudent.firstOrNull { it.first == null }?.second
|
||||
val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } }
|
||||
.filterNotNull().associateBy { it.first.id }
|
||||
val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second
|
||||
val byCrit_ = forStudent
|
||||
.filter { it.first != assignment.globalCriterion.id }
|
||||
.map { Pair(it.first, it.second) }
|
||||
.associateBy { it.first.id }
|
||||
|
||||
val byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: Pair(c, null)
|
||||
}
|
||||
@ -672,7 +694,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
|
||||
it[studentId] = student.id
|
||||
it[this.feedback] = msg ?: ""
|
||||
it[this.grade] = grd ?: ""
|
||||
it[criterionId] = criterion?.id
|
||||
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||
}
|
||||
}
|
||||
feedback.refresh(); autofill.refresh()
|
||||
|
@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
androidx-lifecycle = "2.8.4"
|
||||
compose-multiplatform = "1.7.0"
|
||||
compose-multiplatform = "1.8.1"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.1.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
@ -9,6 +9,7 @@ material3 = "1.7.3"
|
||||
ui-android = "1.7.8"
|
||||
foundation-layout-android = "1.7.8"
|
||||
rtf = "1.0.0-rc11"
|
||||
filekit = "0.10.0-beta04"
|
||||
|
||||
[libraries]
|
||||
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-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", 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" }
|
||||
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
|
||||
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
||||
@ -29,8 +31,12 @@ 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-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" }
|
||||
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]
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
|
Reference in New Issue
Block a user