Compare commits

...
3 Commits
Author SHA1 Message Date
Sweetbread 568ca32668 feat: ViewModel in Schedule.kt 2026-02-16 00:12:22 +03:00
Sweetbread 9b90b655ab wip: cache refactor 2026-02-15 23:28:40 +03:00
Sweetbread 12dc82e5f4 fixup! deps: bump 2026-02-15 15:41:49 +03:00
7 changed files with 207 additions and 272 deletions
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api
@@ -66,11 +66,10 @@ private suspend fun getMyself(login: String) = coroutineScope {
}
suspend fun getScheduleDay(
date: LocalDate,
type: Type = ME.type,
id: Int = ME.unnId!!,
date: LocalDate
): ArrayList<ScheduleUnit> {
id: Int = ME.unnId!!
): List<ScheduleUnit> {
if ((type == ME.type) and (id == ME.unnId!!)) {
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
Log.d("Schedule", schedule.joinToString())
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api
@@ -116,6 +116,11 @@ suspend fun getUser(userId: Int? = null): User {
)
}
/**
* Fetch schedule from Uni API
*
* @return [ArrayList] of all [ScheduleUnit]s between [start] and [finish]
*/
suspend fun downloadSchedule(
type: Type = ME.type,
id: Int = ME.unnId!!,
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api
@@ -29,7 +29,7 @@ class ScheduleUnit(
val date: LocalDate,
val discipline: Discipline,
val kindOfWork: KindOfWork,
val lecturers: ArrayList<Lecturer>,
val lecturers: List<Lecturer>,
val stream: String,
val begin: LocalTime,
val end: LocalTime
+2 -2
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db
@@ -17,7 +17,7 @@ val cacheDb = roomDb<AppDatabase>(name = "cache") {
DisciplineDB::class,
KindOfWorkDB::class,
LecturerDB::class,
ScheduleUnitDB::class], version = 1)
ScheduleUnitDB::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun scheduleDao(): ScheduleItemDao
@@ -1,8 +1,7 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db
import android.util.Log
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
@@ -11,6 +10,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.api.Building
import ru.sweetbread.unn.api.Discipline
@@ -28,7 +28,7 @@ data class BuildingDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val gid: Int,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
@@ -37,40 +37,15 @@ interface BuildingDao {
fun get(oid: Int): BuildingDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(building: BuildingDB)
suspend fun insert(buildings: List<BuildingDB>)
@Delete
fun delete(building: BuildingDB)
}
fun cacheBuilding(building: Building) {
cacheDb.buildingDao().insert(
BuildingDB(
building.oid,
building.name,
building.gid,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun BuildingDB.toModel() = Building(name, gid, oid)
fun Building.toEntity() = BuildingDB(oid, name, gid, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
fun loadBuilding(oid: Int): Building? {
return cacheDb.buildingDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.buildingDao().delete(it)
return null
}
Building(
it.name,
it.gid,
it.oid
)
}
}
@Entity
data class AuditoriumDB(
@@ -78,7 +53,7 @@ data class AuditoriumDB(
@ColumnInfo val name: String,
@ColumnInfo val floor: Int?,
@ColumnInfo val buildingOid: Int,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
@@ -87,43 +62,36 @@ interface AuditoriumDao {
fun get(oid: Int): AuditoriumDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(auditorium: AuditoriumDB)
suspend fun insert(auditoriums: List<AuditoriumDB>)
@Delete
fun delete(auditorium: AuditoriumDB)
}
fun cacheAuditorium(auditorium: Auditorium) {
cacheBuilding(auditorium.building)
cacheDb.auditoriumDao().insert(
AuditoriumDB(
auditorium.oid,
auditorium.name,
auditorium.floor,
auditorium.building.oid,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
fun AuditoriumDB.toModel(): Auditorium? {
return Auditorium(
name,
oid,
floor ?: 0,
cacheDb.buildingDao().get(buildingOid)?.toModel() ?: return null
)
}
fun Auditorium.toEntity() = AuditoriumDB(
oid,
name,
floor,
building.oid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun loadAuditorium(oid: Int): Auditorium? {
return cacheDb.auditoriumDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.auditoriumDao().delete(it)
return null
}
val building = loadBuilding(it.buildingOid) ?: return null
return Auditorium(
it.name,
it.oid,
it.floor ?: 0,
building
suspend fun cacheAuditorium(auditoriums: List<Auditorium>) {
cacheDb.buildingDao().insert(
auditoriums.map { it.building }.map { it.toEntity() }
)
cacheDb.auditoriumDao().insert(
auditoriums.map { it.toEntity() }
)
}
}
@Entity
@@ -131,7 +99,7 @@ data class DisciplineDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val type: Int,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
@@ -140,41 +108,15 @@ interface DisciplineDao {
fun get(oid: Int): DisciplineDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(discipline: DisciplineDB)
suspend fun insert(disciplines: List<DisciplineDB>)
@Delete
fun delete(discipline: DisciplineDB)
}
fun cacheDiscipline(discipline: Discipline) {
cacheDb.disciplineDao().insert(
DisciplineDB(
discipline.oid,
discipline.name,
discipline.type,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun DisciplineDB.toModel() = Discipline(name, oid, type)
fun Discipline.toEntity() = DisciplineDB(oid, name, type, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
fun loadDiscipline(oid: Int): Discipline? {
return cacheDb.disciplineDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.disciplineDao().delete(it)
return null
}
return Discipline(
it.name,
it.oid,
it.type
)
}
}
@Entity
data class KindOfWorkDB(
@@ -182,7 +124,7 @@ data class KindOfWorkDB(
@ColumnInfo val name: String,
@ColumnInfo val uid: String,
@ColumnInfo val complexity: Int,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
@@ -191,43 +133,21 @@ interface KindOfWorkDao {
fun get(oid: Int): KindOfWorkDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(kindOfWork: KindOfWorkDB)
suspend fun insert(kindOfWork: List<KindOfWorkDB>)
@Delete
fun delete(kindOfWork: KindOfWorkDB)
}
fun cacheKindOfWork(kindOfWork: KindOfWork) {
cacheDb.kindOfWorkDao().insert(
KindOfWorkDB(
kindOfWork.oid,
kindOfWork.name,
kindOfWork.uid,
kindOfWork.complexity,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun KindOfWorkDB.toModel() = KindOfWork(name, oid, uid, complexity)
fun KindOfWork.toEntity() = KindOfWorkDB(
oid,
name,
uid,
complexity,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun loadKindOfWork(oid: Int): KindOfWork? {
return cacheDb.kindOfWorkDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.kindOfWorkDao().delete(it)
return null
}
return KindOfWork(
it.name,
it.oid,
it.uid,
it.complexity
)
}
}
@Entity
data class LecturerDB(
@@ -236,7 +156,7 @@ data class LecturerDB(
@ColumnInfo val rank: LecturerRank,
@ColumnInfo val email: String,
@ColumnInfo val uid: String,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
@@ -245,45 +165,22 @@ interface LecturerDao {
fun get(unnId: Int): LecturerDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(lecturer: LecturerDB)
suspend fun insert(lecturers: List<LecturerDB>)
@Delete
fun delete(lecturer: LecturerDB)
}
fun cacheLecturer(lecturer: Lecturer) {
cacheDb.lecturerDao().insert(
LecturerDB(
lecturer.unnId,
lecturer.name,
lecturer.rank,
lecturer.email,
lecturer.uid,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun LecturerDB.toModel() = Lecturer(name, rank, email, unnId, uid)
fun Lecturer.toEntity() = LecturerDB(
unnId,
name,
rank,
email,
uid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun loadLecturer(unnId: Int): Lecturer? {
return cacheDb.lecturerDao().get(unnId)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.lecturerDao().delete(it)
return null
}
return Lecturer(
it.name,
it.rank,
it.email,
it.unnId,
it.uid
)
}
}
@Entity
data class ScheduleUnitDB(
@@ -296,9 +193,9 @@ data class ScheduleUnitDB(
@ColumnInfo val auditoriumOid: Int,
@ColumnInfo val disciplineOid: Int,
@ColumnInfo val kindOfWorkOid: Int,
@ColumnInfo val lecturerId: Int, // TODO: many-to-many
@ColumnInfo val lecturersId: String,
@ColumnInfo val expiredAt: String
@ColumnInfo val updatedAt: String
)
@Dao
interface ScheduleItemDao {
@@ -308,78 +205,52 @@ interface ScheduleItemDao {
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getSchedule(date: String): List<ScheduleUnitDB>
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getScheduleFlow(date: String): Flow<List<ScheduleUnitDB>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user: ScheduleUnitDB)
suspend fun insert(units: List<ScheduleUnitDB>)
@Delete
fun delete(user: ScheduleUnitDB)
fun delete(unit: ScheduleUnitDB)
@Query("DELETE FROM scheduleUnitDB WHERE date = :date")
fun deleteByDate(date: String)
}
fun cacheSchedule(item: ScheduleUnit) {
cacheAuditorium(item.auditorium)
cacheDiscipline(item.discipline)
cacheKindOfWork(item.kindOfWork)
cacheLecturer(item.lecturers[0])
cacheDb.scheduleDao().insert(
ScheduleUnitDB(
item.oid,
item.date.format(DateTimeFormatter.ISO_DATE),
item.stream,
item.begin.format(DateTimeFormatter.ISO_TIME),
item.end.format(DateTimeFormatter.ISO_TIME),
item.auditorium.oid,
item.discipline.oid,
item.kindOfWork.oid,
item.lecturers[0].unnId,
when {
(LocalDate.now() > item.date) -> LocalDateTime.now().plusWeeks(2)
(LocalDate.now().plusWeeks(1) > item.date) -> LocalDateTime.now().plusDays(1)
(LocalDate.now().plusWeeks(2) > item.date) -> LocalDateTime.now().plusWeeks(1)
else -> LocalDateTime.now().plusWeeks(2)
}.format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun cacheSchedule(items: ArrayList<ScheduleUnit>) {
for (item in items)
cacheSchedule(item)
}
fun loadSchedule(oid: Int): ScheduleUnit? {
cacheDb.scheduleDao().getSchedule(oid)?.let {
Log.d("load", it.oid.toString())
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) < LocalDateTime.now()
) {
Log.d("delete", it.oid.toString())
cacheDb.scheduleDao().delete(it)
return null
}
fun ScheduleUnitDB.toModel(): ScheduleUnit? {
return ScheduleUnit(
it.oid,
loadAuditorium(it.auditoriumOid) ?: return null,
LocalDate.parse(it.date, DateTimeFormatter.ISO_DATE),
loadDiscipline(it.disciplineOid) ?: return null,
loadKindOfWork(it.kindOfWorkOid) ?: return null,
arrayListOf(loadLecturer(it.lecturerId) ?: return null),
it.stream,
LocalTime.parse(it.begin, DateTimeFormatter.ISO_TIME),
LocalTime.parse(it.end, DateTimeFormatter.ISO_TIME)
oid,
cacheDb.auditoriumDao().get(auditoriumOid)?.toModel() ?: return null,
LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
cacheDb.disciplineDao().get(disciplineOid)?.toModel() ?: return null,
cacheDb.kindOfWorkDao().get(kindOfWorkOid)?.toModel() ?: return null,
lecturersId.split(",").map { cacheDb.lecturerDao().get(it.toInt())?.toModel() ?: return null },
stream,
LocalTime.parse(begin, DateTimeFormatter.ISO_TIME),
LocalTime.parse(end, DateTimeFormatter.ISO_TIME)
)
}
return null
}
fun ScheduleUnit.toEntity() = ScheduleUnitDB(
oid,
date.format(DateTimeFormatter.ISO_DATE),
stream,
begin.format(DateTimeFormatter.ISO_TIME),
end.format(DateTimeFormatter.ISO_TIME),
auditorium.oid,
discipline.oid,
kindOfWork.oid,
lecturers.map { it.unnId }.joinToString(","),
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
return ArrayList(
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { loadSchedule(it.oid) }
)
suspend fun cacheSchedule(units: List<ScheduleUnit>) {
cacheAuditorium(units.map { it.auditorium })
cacheDb.disciplineDao().insert(units.map { it.discipline.toEntity() })
cacheDb.kindOfWorkDao().insert(units.map { it.kindOfWork.toEntity() })
cacheDb.lecturerDao().insert(units.flatMap { it.lecturers }.map { it.toEntity() })
cacheDb.scheduleDao().insert(units.map { it.toEntity() })
}
fun loadSchedule(date: LocalDate): List<ScheduleUnit> =
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { it.toModel() }
@@ -2,7 +2,6 @@
package ru.sweetbread.unn.ui.composes
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Canvas
@@ -24,12 +23,11 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,9 +41,22 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.api.Building
@@ -55,6 +66,8 @@ import ru.sweetbread.unn.api.Lecturer
import ru.sweetbread.unn.api.LecturerRank
import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.api.getScheduleDay
import ru.sweetbread.unn.db.cacheDb
import ru.sweetbread.unn.db.toModel
import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr
import java.time.LocalDate
@@ -64,33 +77,84 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
class ScheduleRepository {
fun getFromDb(date: LocalDate): List<ScheduleUnit> =
cacheDb.scheduleDao()
.getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { it.toModel() }
suspend fun getFromApi(date: LocalDate): List<ScheduleUnit> = getScheduleDay(date)
}
sealed class ScheduleState {
object Loading : ScheduleState()
data class Success(val data: List<ScheduleUnit>) : ScheduleState()
data class Error(val message: String) : ScheduleState()
}
class ScheduleViewModel : ViewModel() {
private val repository = ScheduleRepository()
private val _date = MutableStateFlow(LocalDate.now())
val date: StateFlow<LocalDate> = _date.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val scheduleState: StateFlow<ScheduleState> = _date
.flatMapLatest { date ->
flow {
emit(ScheduleState.Loading)
val cached = withContext(Dispatchers.IO) {
repository.getFromDb(date)
}
emit(ScheduleState.Success(cached))
try {
val fresh = repository.getFromApi(date)
if (fresh != cached) {
emit(ScheduleState.Success(fresh))
}
} catch (e: Exception) {
emit(ScheduleState.Error("Ошибка загрузки: ${e.message}"))
}
}.flowOn(Dispatchers.IO)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ScheduleState.Loading
)
fun updateDate(newDate: LocalDate) {
_date.update { newDate }
}
}
@Composable
fun Schedule() {
var selectedDate by remember { mutableStateOf(LocalDate.now()) }
val viewModel: ScheduleViewModel = viewModel()
Column {
BoundedWeekPicker(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
selectedDate = selectedDate,
onDateSelected = { selectedDate = it }
selectedDate = viewModel.date.collectAsState().value,
onDateSelected = { viewModel.updateDate(it) }
)
ScheduleDay(date = selectedDate)
ScheduleDay(viewModel = viewModel)
}
}
@Composable
fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
val scope = rememberCoroutineScope()
var loadedDate by remember { mutableStateOf(LocalDate.MIN) }
val lessons = remember { mutableListOf<ScheduleUnit>() }
fun ScheduleDay(modifier: Modifier = Modifier, viewModel: ScheduleViewModel) {
var expanded by remember { mutableIntStateOf(0) }
val state by viewModel.scheduleState.collectAsState()
if (loadedDate == date) {
Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}")
LazyColumn (modifier) {
when (state) {
is ScheduleState.Success -> {
val lessons = (state as ScheduleState.Success).data
LazyColumn(modifier) {
items(lessons) {
ScheduleItem(unit = it, modifier = Modifier.clickable {
expanded = if (it.oid == expanded) 0
@@ -101,7 +165,8 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
if (lessons.isEmpty())
item { Text(appStr(R.string.noData)) }
}
} else {
}
is ScheduleState.Loading ->
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
@@ -109,13 +174,8 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
color = MaterialTheme.colorScheme.surfaceVariant,
trackColor = MaterialTheme.colorScheme.secondary,
)
LaunchedEffect(date != loadedDate) {
scope.launch(Dispatchers.IO) {
lessons.clear()
lessons.addAll(getScheduleDay(date = date))
loadedDate = date
}
}
is ScheduleState.Error ->
Text((state as ScheduleState.Error).message, color = MaterialTheme.colorScheme.error)
}
}
+1 -1
View File
@@ -3,6 +3,6 @@
#Sat Mar 16 18:30:45 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists