This commit is contained in:
2026-09-02 23:47:00 +03:00
parent b4d0b0c8e9
commit a8830bcd91
77 changed files with 2998 additions and 0 deletions
@@ -0,0 +1,152 @@
package agzam.vyatsu.ui.groups
import agzam.vyatsu.domain.model.Group
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Страница выбора группы
* @author Agzam
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GroupPickerScreen(viewModel: GroupPickerViewModel, onGroupPicked: (Group) -> Unit) {
val state by viewModel.uiState.collectAsState()
var isSearchActive by remember { mutableStateOf(false) }
Scaffold { innerPadding ->
Column(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (!isSearchActive) {
Row(
Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Выберите группу",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { viewModel.refresh() }) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
}
}
if (state.isLoading && state.groups.isEmpty()) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
CircularProgressIndicator()
}
return@Column
}
if (state.error != null && state.groups.isEmpty()) {
Column(Modifier.fillMaxWidth()) {
Text(state.error.orEmpty(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error)
TextButton(onClick = { viewModel.retry() }) { Text("Повторить") }
}
return@Column
}
SearchBar(
modifier = Modifier.fillMaxWidth(),
expanded = isSearchActive,
onExpandedChange = { isSearchActive = it },
inputField = {
SearchBarDefaults.InputField(
query = state.searchQuery,
onQueryChange = { viewModel.search(it) },
onSearch = { isSearchActive = false },
expanded = isSearchActive,
onExpandedChange = { isSearchActive = it },
placeholder = { Text("Поиск группы") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (isSearchActive && state.searchQuery.isNotEmpty()) {
IconButton(onClick = { viewModel.search("") }) {
Icon(Icons.Default.Clear, contentDescription = "Очистить")
}
}
}
)
}
) {
GroupList(
groups = state.filteredGroups,
onPick = { group ->
isSearchActive = false
onGroupPicked(group)
}
)
}
if (!isSearchActive) {
Spacer(modifier = Modifier.height(8.dp))
GroupList(state.filteredGroups, onGroupPicked)
}
}
}
}
@Composable
private fun GroupList(groups: List<Group>, onPick: (Group) -> Unit) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(groups) { group ->
Column(modifier = Modifier.fillMaxWidth() .clickable { onPick(group) }) {
Box(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart
) {
Text(group.name, style = MaterialTheme.typography.bodyMedium)
// XXX: стоит ли добавлять факультет или это засорит интерфейс
// Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) {
// if (group.faculty.isNotEmpty()) Text(text = group.faculty + " · ", color = MaterialTheme.colorScheme.onSurfaceVariant)
// }
}
}
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
@@ -0,0 +1,100 @@
package agzam.vyatsu.ui.groups
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.repository.GroupRepository
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* @author Agzam
*/
class GroupPickerViewModel(private val repository: GroupRepository) : ViewModel() {
private val _uiState = MutableStateFlow(GroupPickerUiState())
val uiState: StateFlow<GroupPickerUiState> = _uiState.asStateFlow()
init {
load()
}
private fun load() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val ok = try {
repository.refreshIfStale()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Не удалось загрузить группы"
)
false
}
if (!ok && _uiState.value.groups.isEmpty()) {
_uiState.value = _uiState.value.copy(
error = _uiState.value.error ?: "Не удалось обновить список групп"
)
}
}
repository.observeGroups().collect { groups ->
val query = _uiState.value.searchQuery
_uiState.value = _uiState.value.copy(
isLoading = false,
groups = groups,
filteredGroups = filterGroups(groups, query),
error = null
)
}
}
}
fun search(query: String) {
val groups = _uiState.value.groups
_uiState.value = _uiState.value.copy(
searchQuery = query,
filteredGroups = filterGroups(groups, query)
)
}
private fun filterGroups(groups: List<Group>, query: String): List<Group> {
if (query.isBlank()) return groups
val lowerQuery = query.lowercase()
return groups.filter { group ->
group.name.lowercase().contains(lowerQuery) || group.faculty.lowercase().contains(lowerQuery)
}
}
fun retry() {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
load()
}
fun refresh() {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
repository.forceRefresh()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Не удалось обновить список групп"
)
return@withContext
}
}
}
}
}
data class GroupPickerUiState(
val isLoading: Boolean = true,
val error: String? = null,
val groups: List<Group> = emptyList(),
val searchQuery: String = "",
val filteredGroups: List<Group> = emptyList()
)
@@ -0,0 +1,73 @@
package agzam.vyatsu.ui.home
import agzam.vyatsu.ui.info.BuildingsScreen
import agzam.vyatsu.ui.info.CallsScreen
import agzam.vyatsu.ui.schedule.ScheduleScreen
import agzam.vyatsu.ui.schedule.ScheduleViewModel
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
private enum class HomeTab(val label: String) {
SCHEDULE("Расписание"),
CALLS("Звонки"),
BUILDINGS("Корпуса")
}
@Composable
fun HomeScreen(
scheduleViewModel: ScheduleViewModel,
onSwitchGroup: () -> Unit
) {
var tab by rememberSaveable { mutableStateOf(HomeTab.SCHEDULE) }
Scaffold(
bottomBar = {
NavigationBar {
HomeTab.entries.forEach { t ->
NavigationBarItem(
selected = tab == t,
onClick = { tab = t },
icon = {
Icon(
imageVector = when (t) {
HomeTab.SCHEDULE -> Icons.Default.DateRange
HomeTab.CALLS -> Icons.Default.Info
HomeTab.BUILDINGS -> Icons.Default.Home
},
contentDescription = t.label
)
},
label = { Text(t.label) }
)
}
}
}
) { innerPadding ->
androidx.compose.foundation.layout.Box(
Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when (tab) {
HomeTab.SCHEDULE -> ScheduleScreen(viewModel = scheduleViewModel, onSwitchGroup = onSwitchGroup)
HomeTab.CALLS -> CallsScreen()
HomeTab.BUILDINGS -> BuildingsScreen()
}
}
}
}
@@ -0,0 +1,70 @@
package agzam.vyatsu.ui.info
import agzam.vyatsu.data.static.BUILDINGS
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import agzam.vyatsu.BuildConfig
@Composable
fun BuildingsScreen() {
val context = LocalContext.current
LazyColumn(
Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.statusBars)
) {
items(BUILDINGS) { b ->
Column(
Modifier
.fillMaxWidth()
.clickable {
val uri = Uri.parse("geo:${b.latitude},${b.longitude}?q=${b.latitude},${b.longitude}(Корпус ${b.number})")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
"Корпус ${b.number}",
style = MaterialTheme.typography.bodyLarge
)
Text(
b.address,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
}
item {
Spacer(Modifier.height(24.dp))
Text(
"Версия ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
}
}
}
@@ -0,0 +1,42 @@
package agzam.vyatsu.ui.info
import agzam.vyatsu.data.static.CALLS
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun CallsScreen() {
LazyColumn(
Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.statusBars)
) {
itemsIndexed(CALLS) { index, call ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
Text(
"${index + 1} пара",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
Text(
"${call.start} – ${call.end}",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
}
}
}
@@ -0,0 +1,218 @@
package agzam.vyatsu.ui.schedule
import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.TimeSlot
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.time.DayOfWeek
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
val state by viewModel.uiState.collectAsState()
Column(Modifier.fillMaxSize()) {
TopAppBar(
title = { Text("Расписание") },
actions = {
TextButton(onClick = { onSwitchGroup() }) {
Text("Сменить группу")
}
IconButton(onClick = { viewModel.refresh() }) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
},
windowInsets = WindowInsets.systemBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Top)
)
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) {
when {
state.isSyncing && state.schedule == null -> {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
}
}
state.schedule == null -> {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Не удалось загрузить расписание",
style = MaterialTheme.typography.bodyLarge
)
TextButton(onClick = { viewModel.refresh() }) {
Text("Повторить")
}
}
}
else -> {
val schedule = state.schedule!!
val day = schedule.days.getOrNull(state.selectedIndex)
if (day == null) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else {
val isToday = state.selectedIndex == state.todayIndex
val dayLabel = if (isToday) "Сегодня " else ""
Text(
"$dayLabel ${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}",
style = MaterialTheme.typography.titleMedium
)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
DayChips(
days = schedule.days,
selectedIndex = state.selectedIndex,
todayIndex = state.todayIndex,
onSelect = viewModel::selectDay
)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
if (day.intervals.all { it.lessons.isEmpty() }) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(day.intervals) { slotIndex, slot ->
LessonSlot(slotIndex = slotIndex, slot = slot)
}
}
}
}
}
}
}
}
}
@Composable
private fun DayChips(
days: List<Day>,
selectedIndex: Int,
todayIndex: Int,
onSelect: (Int) -> Unit
) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
itemsIndexed(days) { index, day ->
val selected = selectedIndex == index
val isTodayChip = todayIndex == index
FilterChip(
selected = selected,
onClick = { onSelect(index) },
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)
)
}
}
}
@Composable
private fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
Column(Modifier.fillMaxWidth()) {
Text(
"${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
if (slot.lessons.isEmpty()) {
Text("—", style = MaterialTheme.typography.bodyMedium)
} else {
slot.lessons.forEach { lesson ->
Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
Text(
lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw,
style = MaterialTheme.typography.bodyLarge
)
Text(
listOf(lesson.type.label, lesson.subgroup, lesson.teacher, lesson.room)
.filterNotNull()
.filter { it.isNotBlank() }
.joinToString(" · "),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
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,113 @@
package agzam.vyatsu.ui.schedule
import agzam.vyatsu.domain.ScheduleDates
import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.Schedule
import agzam.vyatsu.domain.repository.PreferencesRepository
import agzam.vyatsu.domain.repository.ScheduleRepository
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ScheduleViewModel(
private val scheduleRepository: ScheduleRepository,
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ScheduleUiState())
val uiState: StateFlow<ScheduleUiState> = _uiState.asStateFlow()
private var currentGroupId: String? = null
init {
viewModelScope.launch {
val groupId = preferencesRepository.selectedGroupId()
if (groupId != null) {
selectGroup(groupId)
} else {
_uiState.value = _uiState.value.copy(noGroup = true)
}
}
}
fun selectGroup(groupId: String) {
currentGroupId = groupId
viewModelScope.launch {
preferencesRepository.setSelectedGroup(groupId)
_uiState.value = _uiState.value.copy(noGroup = false)
launch {
scheduleRepository.observeSchedule(groupId).collect { schedule ->
currentGroupId?.let {
val days = schedule?.days.orEmpty()
val today = todayIndex(days)
val current = _uiState.value
val selected = current.selectedIndex.takeIf { i -> i in days.indices } ?: today
_uiState.value = current.copy(
schedule = schedule,
todayIndex = today,
selectedIndex = selected
)
}
}
}
launch {
_uiState.value = _uiState.value.copy(isSyncing = true)
scheduleRepository.sync(groupId)
_uiState.value = _uiState.value.copy(isSyncing = false)
}
}
}
fun switchGroup() {
viewModelScope.launch {
currentGroupId = null
preferencesRepository.clearSelectedGroup()
_uiState.value = _uiState.value.copy(noGroup = true)
}
}
fun refresh() {
val groupId = currentGroupId ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
scheduleRepository.sync(groupId)
val current = _uiState.value
val days = current.schedule?.days.orEmpty()
val today = todayIndex(days)
_uiState.value = current.copy(
isRefreshing = false,
todayIndex = today,
selectedIndex = current.selectedIndex.takeIf { it in days.indices } ?: today
)
}
}
fun selectDay(index: Int) {
val days = _uiState.value.schedule?.days.orEmpty()
if (index !in days.indices) return
_uiState.value = _uiState.value.copy(selectedIndex = index)
}
private fun todayIndex(days: List<Day>): Int {
if (days.isEmpty()) return 0
val today = ScheduleDates.moscowToday()
val exact = days.indexOfFirst { it.date == today }
if (exact >= 0) return exact
val next = days.indexOfFirst { !it.date.isBefore(today) }
return if (next >= 0) next else days.lastIndex
}
}
data class ScheduleUiState(
val noGroup: Boolean = false,
val isRefreshing: Boolean = false,
val isSyncing: Boolean = false,
val schedule: Schedule? = null,
val todayIndex: Int = 0,
val selectedIndex: Int = 0
)
@@ -0,0 +1,11 @@
package agzam.vyatsu.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package agzam.vyatsu.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun VyatsuScheduleTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package agzam.vyatsu.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)