package ritm.schedule.ui import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import ritm.schedule.data.Display import ritm.schedule.ui.theme.harmonizeWith import ritm.schedule.data.Lesson import ritm.schedule.data.Timetable import ritm.schedule.data.parseTime import java.time.LocalTime /** * A subject's colours: [container]/[content] for small strong fills (week grid, dots), * [soft]/[onSoft] for School Planner style lesson cards. Every pair keeps 4.5:1 contrast. */ data class SubjectColor(val container: Color, val content: Color, val soft: Color, val onSoft: Color) // Material 700 tones (strong in light theme) and 200 tones (strong in dark theme). private val TONE_700 = listOf( 0xFF1565C0, 0xFF7B1FA2, 0xFFC62828, 0xFF2E7D32, 0xFFD84315, 0xFF00695C, 0xFF283593, 0xFFAD1457, 0xFF5D4037, 0xFF00838F, 0xFF4527A0, 0xFF455A64, ).map { Color(it) } private val TONE_200 = listOf( 0xFF90CAF9, 0xFFCE93D8, 0xFFEF9A9A, 0xFFA5D6A7, 0xFFFFAB91, 0xFF80CBC4, 0xFF9FA8DA, 0xFFF48FB1, 0xFFBCAAA4, 0xFF80DEEA, 0xFFB39DDB, 0xFFB0BEC5, ).map { Color(it) } private val LIGHT = TONE_700.indices.map { i -> SubjectColor( container = TONE_700[i], content = Color.White, soft = lerp(TONE_200[i], Color.White, 0.35f), onSoft = lerp(TONE_700[i], Color.Black, 0.35f), ) } private val DARK = TONE_700.indices.map { i -> SubjectColor( container = TONE_200[i], content = Color(0xFF1D1B20), soft = lerp(Color(0xFF211F26), TONE_700[i], 0.5f), onSoft = lerp(TONE_200[i], Color.White, 0.4f), ) } fun subjectKey(subject: String) = subject.trim().lowercase() /** * Palette slot of every subject of the open timetable. Slots go in alphabetical order, * so a subject keeps its colour across refreshes and two subjects rarely share one. */ fun subjectSlots(t: Timetable?): Map { if (t == null) return emptyMap() val subjects = (t.days.flatMap { it.lessons } + t.weekly.flatMap { it.lessons }) .map { subjectKey(it.subject) } .distinct() .sorted() return subjects.withIndex().associate { (i, key) -> key to i % LIGHT.size } } val LocalSubjectSlots = staticCompositionLocalOf> { emptyMap() } /** The student's display choices from the settings. */ val LocalDisplay = staticCompositionLocalOf { Display() } @Composable fun subjectColor(subject: String): SubjectColor { if (!LocalDisplay.current.colors) { // Colours off: every lesson in the calm theme tones. val scheme = MaterialTheme.colorScheme return SubjectColor(scheme.primaryContainer, scheme.onPrimaryContainer, scheme.surfaceContainerHigh, scheme.onSurface) } val key = subjectKey(subject) // Subjects of other groups (search results) still get a stable colour. val slot = LocalSubjectSlots.current[key] ?: key.hashCode().mod(LIGHT.size) val dark = MaterialTheme.colorScheme.surface.luminance() < 0.5f return harmonized(dark, MaterialTheme.colorScheme.primary)[slot] } // Harmonized palettes per theme and primary; the primary changes only with the theme or wallpaper. private val palettes = HashMap, List>() private fun harmonized(dark: Boolean, primary: Color): List = synchronized(palettes) { palettes.getOrPut(dark to primary) { (if (dark) DARK else LIGHT).map { SubjectColor( container = it.container.harmonizeWith(primary), content = it.content, soft = it.soft.harmonizeWith(primary), onSoft = it.onSoft.harmonizeWith(primary), ) } } } private val SHORT_WORDS = setOf("и", "в", "во", "на", "по", "с", "со", "к", "о", "об", "для", "из", "от") /** "Экономика предприятия в машиностроении" → "ЭПМ"; short names stay as they are. */ fun abbreviate(subject: String): String { val name = subject.substringBefore(':').trim() if (name.length <= 10) return name val letters = name.split(' ', '-', '(', ')', ',') .filter { it.isNotEmpty() && it.lowercase() !in SHORT_WORDS && it.first().isLetter() } .joinToString("") { it.first().uppercase() } // Four letters still fit a phone's week column at the default font size. return letters.take(4).takeIf { it.length >= 2 } ?: name.take(8) } /** End of a lesson: its own end, else the bell table's, else 80 minutes after the start. */ fun lessonEnd(lesson: Lesson, t: Timetable): LocalTime = lesson.end?.let(::parseTime) ?: t.bells.firstOrNull { it.start == lesson.start }?.end?.let(::parseTime) ?: parseTime(lesson.start).plusMinutes(80) fun Color.luminance(): Float = 0.2126f * red + 0.7152f * green + 0.0722f * blue