refactor(ui): extract shared schedule composables
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
package agzam.vyatsu.ui.schedule
|
||||
|
||||
import agzam.vyatsu.domain.model.Day
|
||||
import agzam.vyatsu.domain.model.LessonType
|
||||
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||
import agzam.vyatsu.domain.model.TimeSlot
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import agzam.vyatsu.ui.schedule.components.DayChips
|
||||
import agzam.vyatsu.ui.schedule.components.FULL_DATE
|
||||
import agzam.vyatsu.ui.schedule.components.LessonSlot
|
||||
import agzam.vyatsu.ui.schedule.components.WeekSelector
|
||||
import agzam.vyatsu.ui.schedule.components.dayName
|
||||
import agzam.vyatsu.ui.schedule.components.hPadding
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -16,39 +14,19 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -57,27 +35,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
val hPadding = 16.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -226,290 +187,3 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WeekSelector(
|
||||
periods: List<SchedulePeriod>,
|
||||
selectedWeekStart: LocalDate?,
|
||||
todayWeekStart: LocalDate?,
|
||||
onSelect: (LocalDate) -> Unit
|
||||
) {
|
||||
val selected = periods.firstOrNull { it.weekStart == selectedWeekStart }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(enabled = true, onClick = { expanded = true })
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
selected?.let(::weekLabel) ?: "Период",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.widthIn(min = 220.dp)
|
||||
) {
|
||||
periods.forEach { period ->
|
||||
val isTodayWeek = todayWeekStart == period.weekStart
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
weekLabel(period),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
trailingIcon = if (isTodayWeek) {
|
||||
{
|
||||
Text(
|
||||
"сегодня",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = {
|
||||
expanded = false
|
||||
onSelect(period.weekStart)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVisiblePercentage(item: LazyListItemInfo, layoutInfo: LazyListLayoutInfo): Float {
|
||||
val viewportStart = layoutInfo.viewportStartOffset
|
||||
val viewportEnd = layoutInfo.viewportEndOffset
|
||||
|
||||
val itemStart = item.offset
|
||||
val itemEnd = item.offset + item.size
|
||||
|
||||
val visibleStart = max(itemStart, viewportStart)
|
||||
val visibleEnd = min(itemEnd, viewportEnd)
|
||||
val visibleSize = visibleEnd - visibleStart
|
||||
|
||||
return if (visibleSize <= 0) 0f else visibleSize.toFloat() / item.size
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayChips(
|
||||
days: List<Day>,
|
||||
selectedDate: LocalDate?,
|
||||
todayDate: LocalDate?,
|
||||
pagerState: PagerState,
|
||||
onSelect: (LocalDate) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val paddingPx = with(density) { hPadding.toPx() }
|
||||
|
||||
|
||||
LaunchedEffect(pagerState, days) {
|
||||
snapshotFlow {
|
||||
pagerState.currentPage
|
||||
}.collectLatest { targetPage ->
|
||||
|
||||
if (targetPage in days.indices) {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val visibleItems = layoutInfo.visibleItemsInfo
|
||||
val targetItem = visibleItems.firstOrNull { it.index == targetPage }
|
||||
|
||||
if (targetItem != null) {
|
||||
val visiblePercentage = getVisiblePercentage(targetItem, layoutInfo)
|
||||
val minVisibleThreshold = 0.9f
|
||||
|
||||
if (visiblePercentage < minVisibleThreshold) {
|
||||
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val halfViewport = viewPortWidth / 2f
|
||||
val centerOffset = (halfViewport - (targetItem.size / 2f) - paddingPx).toInt()
|
||||
|
||||
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
|
||||
}
|
||||
} else {
|
||||
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val halfViewport = viewPortWidth / 2f
|
||||
|
||||
val averageSize = visibleItems.map { it.size }.average().toInt().coerceAtLeast(150)
|
||||
val centerOffset = (halfViewport - (averageSize / 2f) - paddingPx).toInt()
|
||||
|
||||
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
|
||||
items(days, key = { it.date.toEpochDay() }) { day ->
|
||||
val selected = selectedDate == day.date
|
||||
val isTodayChip = todayDate == day.date
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = { onSelect(day.date) },
|
||||
label = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
dayName(day.date.dayOfWeek),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
day.date.format(CHIP_DATE),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
border = if (isTodayChip && !selected) {
|
||||
FilterChipDefaults.filterChipBorder(
|
||||
borderColor = MaterialTheme.colorScheme.primary,
|
||||
borderWidth = 1.dp,
|
||||
enabled = true,
|
||||
selected = false
|
||||
)
|
||||
} else {
|
||||
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected)
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun weekLabel(period: SchedulePeriod): String {
|
||||
val first = period.weekStart
|
||||
return "с ${first.format(CHIP_DATE)}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
|
||||
val context = LocalContext.current
|
||||
var indexColor = 0
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.width(10.dp + hPadding).padding(start = hPadding),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.primary, modifier = Modifier.weight(1f).padding(end = hPadding))
|
||||
}
|
||||
if (slot.lessons.isEmpty()) {
|
||||
Text("—", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
slot.lessons.forEachIndexed { index, lesson ->
|
||||
var baseSurface = Modifier.fillMaxWidth().padding(vertical = 2.dp)
|
||||
if ((indexColor) % 2 != 0) {
|
||||
baseSurface = baseSurface.background(
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
);
|
||||
}
|
||||
indexColor++
|
||||
// XXX:
|
||||
// Убрать SelectionContainer?
|
||||
// Режим редактирования расписания?
|
||||
// Меню на onClick?
|
||||
// Меню на onLongClick?
|
||||
SelectionContainer() {
|
||||
Column(
|
||||
baseSurface
|
||||
// XXX: combinedClickable?
|
||||
//.combinedClickable(
|
||||
// onClick = {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// lesson.raw,
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// },
|
||||
// onLongClick = {},
|
||||
// // TODO: onLongClickLabel = "(Accessibility)"
|
||||
//),
|
||||
) {
|
||||
var any = false
|
||||
|
||||
val meta = listOfNotNull(
|
||||
if (lesson.type == LessonType.OTHER) {
|
||||
null
|
||||
} else {
|
||||
lesson.type.label
|
||||
},
|
||||
lesson.subgroup,
|
||||
lesson.teacher,
|
||||
lesson.room
|
||||
).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
|
||||
if (meta.isNotEmpty()) {
|
||||
Text(
|
||||
meta,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
any = true
|
||||
}
|
||||
|
||||
val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw
|
||||
if (subject.isNotEmpty()) {
|
||||
Text(
|
||||
subject,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
any = true
|
||||
}
|
||||
|
||||
if (!any) {
|
||||
Text(
|
||||
"пусто",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dayName(dayOfWeek: DayOfWeek): String = when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> "Пн"
|
||||
DayOfWeek.TUESDAY -> "Вт"
|
||||
DayOfWeek.WEDNESDAY -> "Ср"
|
||||
DayOfWeek.THURSDAY -> "Чт"
|
||||
DayOfWeek.FRIDAY -> "Пт"
|
||||
DayOfWeek.SATURDAY -> "Сб"
|
||||
DayOfWeek.SUNDAY -> "Вс"
|
||||
}
|
||||
|
||||
private val FULL_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||
private val CHIP_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM")
|
||||
private val TIME: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.domain.model.Day
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.time.LocalDate
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
internal fun getVisiblePercentage(item: LazyListItemInfo, layoutInfo: LazyListLayoutInfo): Float {
|
||||
val viewportStart = layoutInfo.viewportStartOffset
|
||||
val viewportEnd = layoutInfo.viewportEndOffset
|
||||
|
||||
val itemStart = item.offset
|
||||
val itemEnd = item.offset + item.size
|
||||
|
||||
val visibleStart = max(itemStart, viewportStart)
|
||||
val visibleEnd = min(itemEnd, viewportEnd)
|
||||
val visibleSize = visibleEnd - visibleStart
|
||||
|
||||
return if (visibleSize <= 0) 0f else visibleSize.toFloat() / item.size
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DayChips(
|
||||
days: List<Day>,
|
||||
selectedDate: LocalDate?,
|
||||
todayDate: LocalDate?,
|
||||
pagerState: PagerState,
|
||||
onSelect: (LocalDate) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val paddingPx = with(density) { hPadding.toPx() }
|
||||
|
||||
|
||||
LaunchedEffect(pagerState, days) {
|
||||
snapshotFlow {
|
||||
pagerState.currentPage
|
||||
}.collectLatest { targetPage ->
|
||||
|
||||
if (targetPage in days.indices) {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val visibleItems = layoutInfo.visibleItemsInfo
|
||||
val targetItem = visibleItems.firstOrNull { it.index == targetPage }
|
||||
|
||||
if (targetItem != null) {
|
||||
val visiblePercentage = getVisiblePercentage(targetItem, layoutInfo)
|
||||
val minVisibleThreshold = 0.9f
|
||||
|
||||
if (visiblePercentage < minVisibleThreshold) {
|
||||
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val halfViewport = viewPortWidth / 2f
|
||||
val centerOffset = (halfViewport - (targetItem.size / 2f) - paddingPx).toInt()
|
||||
|
||||
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
|
||||
}
|
||||
} else {
|
||||
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val halfViewport = viewPortWidth / 2f
|
||||
|
||||
val averageSize = visibleItems.map { it.size }.average().toInt().coerceAtLeast(150)
|
||||
val centerOffset = (halfViewport - (averageSize / 2f) - paddingPx).toInt()
|
||||
|
||||
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
|
||||
items(days, key = { it.date.toEpochDay() }) { day ->
|
||||
val selected = selectedDate == day.date
|
||||
val isTodayChip = todayDate == day.date
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = { onSelect(day.date) },
|
||||
label = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
dayName(day.date.dayOfWeek),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
day.date.format(CHIP_DATE),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
border = if (isTodayChip && !selected) {
|
||||
FilterChipDefaults.filterChipBorder(
|
||||
borderColor = MaterialTheme.colorScheme.primary,
|
||||
borderWidth = 1.dp,
|
||||
enabled = true,
|
||||
selected = false
|
||||
)
|
||||
} else {
|
||||
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected)
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.domain.model.LessonType
|
||||
import agzam.vyatsu.domain.model.TimeSlot
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
internal fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
|
||||
val context = LocalContext.current
|
||||
var indexColor = 0
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.width(10.dp + hPadding).padding(start = hPadding),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.primary, modifier = Modifier.weight(1f).padding(end = hPadding))
|
||||
}
|
||||
if (slot.lessons.isEmpty()) {
|
||||
Text("—", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
slot.lessons.forEachIndexed { index, lesson ->
|
||||
var baseSurface = Modifier.fillMaxWidth().padding(vertical = 2.dp)
|
||||
if ((indexColor) % 2 != 0) {
|
||||
baseSurface = baseSurface.background(
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
);
|
||||
}
|
||||
indexColor++
|
||||
// XXX:
|
||||
// Убрать SelectionContainer?
|
||||
// Режим редактирования расписания?
|
||||
// Меню на onClick?
|
||||
// Меню на onLongClick?
|
||||
SelectionContainer() {
|
||||
Column(
|
||||
baseSurface
|
||||
// XXX: combinedClickable?
|
||||
//.combinedClickable(
|
||||
// onClick = {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// lesson.raw,
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// },
|
||||
// onLongClick = {},
|
||||
// // TODO: onLongClickLabel = "(Accessibility)"
|
||||
//),
|
||||
) {
|
||||
var any = false
|
||||
|
||||
val meta = listOfNotNull(
|
||||
if (lesson.type == LessonType.OTHER) {
|
||||
null
|
||||
} else {
|
||||
lesson.type.label
|
||||
},
|
||||
lesson.subgroup,
|
||||
lesson.teacher,
|
||||
lesson.room
|
||||
).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
|
||||
if (meta.isNotEmpty()) {
|
||||
Text(
|
||||
meta,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
any = true
|
||||
}
|
||||
|
||||
val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw
|
||||
if (subject.isNotEmpty()) {
|
||||
Text(
|
||||
subject,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
any = true
|
||||
}
|
||||
|
||||
if (!any) {
|
||||
Text(
|
||||
"пусто",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = hPadding)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.DayOfWeek
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal val hPadding = 16.dp
|
||||
|
||||
internal fun dayName(dayOfWeek: DayOfWeek): String = when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> "Пн"
|
||||
DayOfWeek.TUESDAY -> "Вт"
|
||||
DayOfWeek.WEDNESDAY -> "Ср"
|
||||
DayOfWeek.THURSDAY -> "Чт"
|
||||
DayOfWeek.FRIDAY -> "Пт"
|
||||
DayOfWeek.SATURDAY -> "Сб"
|
||||
DayOfWeek.SUNDAY -> "Вс"
|
||||
}
|
||||
|
||||
internal val FULL_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||
internal val CHIP_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM")
|
||||
internal val TIME: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
@@ -0,0 +1,96 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun WeekSelector(
|
||||
periods: List<SchedulePeriod>,
|
||||
selectedWeekStart: LocalDate?,
|
||||
todayWeekStart: LocalDate?,
|
||||
onSelect: (LocalDate) -> Unit
|
||||
) {
|
||||
val selected = periods.firstOrNull { it.weekStart == selectedWeekStart }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(enabled = true, onClick = { expanded = true })
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
selected?.let(::weekLabel) ?: "Период",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.widthIn(min = 220.dp)
|
||||
) {
|
||||
periods.forEach { period ->
|
||||
val isTodayWeek = todayWeekStart == period.weekStart
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
weekLabel(period),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
trailingIcon = if (isTodayWeek) {
|
||||
{
|
||||
Text(
|
||||
"сегодня",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = {
|
||||
expanded = false
|
||||
onSelect(period.weekStart)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun weekLabel(period: SchedulePeriod): String {
|
||||
val first = period.weekStart
|
||||
return "с ${first.format(CHIP_DATE)}"
|
||||
}
|
||||
Reference in New Issue
Block a user