Compare commits

..
Author SHA1 Message Date
vantablackTea 8af24d06e3 made merge error, testing 2026-09-06 00:48:14 +03:00
vantablackTea 4e180c8b9b vibecoded stable updater in settings 2026-09-06 00:27:43 +03:00
38 changed files with 211 additions and 691 deletions
+3 -11
View File
@@ -30,17 +30,9 @@ jobs:
# Ставим JDK вручную и явно прописываем JAVA_HOME.
# actions/setup-java не всегда передаёт JAVA_HOME дальше в Gitea Actions.
run: |
set -euxo pipefail
cd /tmp
wget -q https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.12%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz
tar -xzf OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz -C /opt
mv /opt/jdk-17.0.12+7 /opt/java17
export JAVA_HOME=/opt/java17
export PATH=$JAVA_HOME/bin:$PATH
echo "JAVA_HOME=/opt/java17" >> $GITHUB_ENV
echo "$JAVA_HOME/bin" >> $GITHUB_PATH
java -version
javac -version
apt-get update
apt-get install -y openjdk-17-jdk-headless unzip wget
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV"
- name: Install Android SDK
# Устанавливаем command-line tools и нужные компоненты SDK,
+5 -1
View File
@@ -1,2 +1,6 @@
# vyatsu-schedule
# vyatsu-schedulegit
## settings-branch
blank settings at first, soon will add update feature.
settings will be presetned as fourth button on the bottom of the screen.
-1
View File
@@ -53,7 +53,6 @@ android {
}
testOptions {
unitTests.isReturnDefaultValues = true
unitTests.all { it.maxHeapSize = "16m" }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
+1 -1
View File
@@ -10,7 +10,7 @@
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/icon"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

+1 -28
View File
@@ -6,9 +6,6 @@ import agzam.vyatsu.ui.groups.GroupPickerViewModel
import agzam.vyatsu.ui.home.HomeScreen
import agzam.vyatsu.ui.schedule.ScheduleViewModel
import agzam.vyatsu.ui.settings.UpdateViewModel
import androidx.compose.material3.AlertDialog
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
@@ -23,36 +20,12 @@ fun AppRoot() {
val container = app.container
val scheduleViewModel: ScheduleViewModel = viewModel(factory = scheduleFactory(container))
val groupPickerViewModel: GroupPickerViewModel = viewModel(factory = groupPickerFactory(container))
val updateViewModel: UpdateViewModel = viewModel(factory = updateFactory(container))
val state by scheduleViewModel.uiState.collectAsState()
val showFirstRunDialog by updateViewModel.showFirstRunDialog.collectAsState()
if (showFirstRunDialog) {
AlertDialog(
onDismissRequest = { updateViewModel.onFirstRunDecision(false) },
title = { Text("Автоматические обновления") },
text = {
Text(
"Разрешить автоматически проверять и устанавливать " +
"новые версии приложения?"
)
},
confirmButton = {
TextButton(onClick = { updateViewModel.onFirstRunDecision(true) }) {
Text("Разрешить")
}
},
dismissButton = {
TextButton(onClick = { updateViewModel.onFirstRunDecision(false) }) {
Text("Не надо")
}
}
)
}
if (state.noGroup) {
val groupPickerViewModel: GroupPickerViewModel = viewModel(factory = groupPickerFactory(container))
GroupPickerScreen(
viewModel = groupPickerViewModel,
onGroupPicked = { group -> scheduleViewModel.selectGroup(group.id) }
@@ -33,10 +33,4 @@ interface PreferencesDao {
@Query("INSERT OR REPLACE INTO preferences(key, value) VALUES('autoUpdateEnabled', :enabled)")
suspend fun setAutoUpdateEnabled(enabled: String)
@Query("SELECT value FROM preferences WHERE key = 'autoUpdateAsked'")
suspend fun autoUpdateAsked(): String?
@Query("INSERT OR REPLACE INTO preferences(key, value) VALUES('autoUpdateAsked', :asked)")
suspend fun setAutoUpdateAsked(asked: String)
}
@@ -3,7 +3,8 @@ package agzam.vyatsu.data.remote
import agzam.vyatsu.domain.model.DateRange
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Season
import java.io.InputStream
import android.util.Log
import org.jsoup.Jsoup
/**
* Основано на https://github.com/alirzaev/vyatsu-schedule-backend/blob/master/utils/extractors.py
@@ -13,13 +14,6 @@ object GroupsParser {
private val groupPattern = Regex("""/reports/schedule/Group/(\d{4,})_([12])_(\d{8})_(\d{8})\.pdf""")
private val grpIdPattern = Regex("""data-grp_period_id="(\d+)"""")
private val parenthesized = Regex("""\(.*?\)\s?""")
private const val CHUNK_SIZE = 8192
private const val MAX_SLICE = 1 shl 16 // 64 KiB — защита от аномально длинных единиц
private const val MAX_PENDING = 1 shl 20 // 1 MiB — жёсткий потолок буфера
private val shorthands = mapOf(
"Институт биологии и биотехнологии (факультет)(ОРУ)" to "ИББТ",
"Институт химии и экологии (факультет) (ОРУ)" to "ИнХимЭк",
@@ -39,118 +33,41 @@ object GroupsParser {
)
/**
* Стриминговый парсер страницы групп: читает тело ответа по чанкам и
* разбирает его на лету, не материализуя весь HTML в память.
*
* Страница весит ~2.6MB, а на устройствах с маленькой кучей даже
* одиночный String такого размера — критично (jsoup-DOM и подавно приводил к OOM).
* За один проход извлекаем и список групп, и диапазоны дат из PDF-ссылок.
* Извлекает список групп с HTML страницы
*/
fun parseStream(input: InputStream): ParsedGroups {
val reader = input.bufferedReader(Charsets.UTF_8)
val buf = CharArray(CHUNK_SIZE)
val pending = StringBuilder(CHUNK_SIZE * 2)
val groups = ArrayList<Group>()
val ranges = HashMap<String, HashMap<Season, MutableList<DateRange>>>()
var faculty = ""
fun parseGroups(html: String): List<Group> {
val doc = Jsoup.parse(html)
val container = doc.selectFirst("div.column-center_rasp") ?: return emptyList()
val tables = container.select("table[style~=border: none !important]")
fun addRanges(text: String) {
for (m in groupPattern.findAll(text)) {
val groupId = m.groupValues[1]
val season = if (m.groupValues[2] == "1") Season.AUTUMN else Season.SPRING
val range = DateRange(m.groupValues[3], m.groupValues[4])
ranges.getOrPut(groupId) {
hashMapOf(Season.AUTUMN to ArrayList(), Season.SPRING to ArrayList())
}.getOrPut(season) { ArrayList() }.add(range)
}
}
return tables.flatMap { table ->
table.select("> tbody > tr, > tr").flatMap { row ->
Log.v("ROW", row.html());
val faculty = shortname(row.selectFirst("div.fak_name")?.text().orEmpty()) //
while (true) {
val n = reader.read(buf)
if (n < 0) break
pending.append(buf, 0, n)
var pos = 0
while (pos < pending.length) {
val divAt = pending.indexOf("<div", pos)
val aAt = pending.indexOf("<a", pos)
val tagStart = when {
divAt < 0 -> aAt
aAt < 0 -> divAt
else -> minOf(divAt, aAt)
}
if (tagStart < 0) {
// хвостовой текст без div-тегов — обычный текст страницы
pos = pending.length
break
}
if (tagStart > pos) {
addRanges(pending.substring(pos, tagStart))
}
val tagEnd = findTagEnd(pending, tagStart)
if (tagEnd < 0) break // тег не дочитан — ждём следующий чанк
val tagText = pending.substring(tagStart, tagEnd + 1)
val isDiv = tagText.startsWith("<div")
val isFakName = isDiv && tagText.contains("class=\"fak_name\"")
val isGrpPeriod = isDiv && tagText.contains("class=\"grpPeriod\"")
if (isFakName || isGrpPeriod) {
val closeDiv = pending.indexOf("</div>", tagEnd + 1)
if (closeDiv < 0) break // содержимое не дочитано — ждём следующий чанк
addRanges(tagText)
val content = pending.substring(tagEnd + 1, closeDiv)
if (isFakName) {
val name = content.trim()
if (name.isNotEmpty()) faculty = shortname(name)
} else {
val m = grpIdPattern.find(tagText)
if (m != null) {
groups += Group(
id = m.groupValues[1].dropLast(1),
name = removeParenthesized(content),
row.select("div.grpPeriod").map { grp ->
Group(
id = grp.attr("data-grp_period_id").dropLast(1),
name = removeParenthesized(grp.text()),
faculty = faculty
)
}
}
pos = closeDiv + 6
} else {
addRanges(tagText)
pos = tagEnd + 1
}
}
if (pos > MAX_SLICE) {
pending.delete(0, pos)
pos = 0
fun parseDateRanges(html: String): Map<String, Map<Season, List<DateRange>>> {
val result = HashMap<String, HashMap<Season, MutableList<DateRange>>>()
groupPattern.findAll(html).forEach { m ->
val groupId = m.groupValues[1]
val season = if (m.groupValues[2] == "1") Season.AUTUMN else Season.SPRING
val range = DateRange(m.groupValues[3], m.groupValues[4])
result
.getOrPut(groupId) { hashMapOf(Season.AUTUMN to mutableListOf(), Season.SPRING to mutableListOf()) }
.getOrPut(season) { mutableListOf() }
.add(range)
}
}
if (pos > 0 && pos <= pending.length) {
pending.delete(0, pos)
}
if (pending.length > MAX_PENDING) {
// на странице нет настолько длинных конструкций; страховка от аномалий
pending.delete(0, pending.length - MAX_SLICE)
}
}
return ParsedGroups(groups, ranges)
}
/** Ищет конец HTML-тега — символ '>' вне кавычек. */
private fun findTagEnd(s: CharSequence, start: Int): Int {
var i = start
var quote = 0.toChar()
while (i < s.length) {
val c = s[i]
if (quote != 0.toChar()) {
if (c == quote) quote = 0.toChar()
} else {
when (c) {
'"', '\'' -> quote = c
'>' -> return i
}
}
i++
}
return -1
return result
}
private fun shortname(name: String): String {
@@ -159,10 +76,5 @@ object GroupsParser {
return if (sh != null) "$sh ($stripped)" else stripped
}
private fun removeParenthesized(name: String): String = name.replace(parenthesized, "").trim()
private fun removeParenthesized(name: String): String = name.replace(Regex("""\(.*?\)\s?"""), "").trim()
}
data class ParsedGroups(
val groups: List<Group>,
val dateRanges: Map<String, Map<Season, List<DateRange>>>
)
@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
import java.util.concurrent.TimeUnit
/**
@@ -36,15 +35,4 @@ object VyatsuHttpClient {
}
}
/**
* Открывает потоковое тело ответа. Вызывающий обязан закрыть
* возвращённый [InputStream], чтобы вернуть соединение в пул.
*/
suspend fun openStream(url: String): InputStream = withContext(Dispatchers.IO) {
val resp = client.newCall(Request.Builder().url(url).build()).execute()
Log.d(TAG, "GET $url -> ${resp.code}")
check(resp.isSuccessful) { "HTTP ${resp.code} for $url" }
resp.body!!.byteStream()
}
}
@@ -9,11 +9,9 @@ import agzam.vyatsu.domain.ScheduleDates
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Season
import agzam.vyatsu.domain.repository.GroupRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class GroupRepositoryImpl(
private val groupDao: GroupDao,
@@ -42,11 +40,11 @@ class GroupRepositoryImpl(
}
private suspend fun refresh() {
val parsed = withContext(Dispatchers.IO) {
VyatsuHttpClient.openStream(VyatsuUrls.GROUPS_PAGE).use(GroupsParser::parseStream)
}
val entities = parsed.groups.map { group ->
val range = parsed.dateRanges[group.id]
val html = VyatsuHttpClient.getText(VyatsuUrls.GROUPS_PAGE)
val groups = GroupsParser.parseGroups(html)
val ranges = GroupsParser.parseDateRanges(html)
val entities = groups.map { group ->
val range = ranges[group.id]
val autumn = range?.get(Season.AUTUMN)?.lastOrNull()
val spring = range?.get(Season.SPRING)?.lastOrNull()
GroupEntity(
@@ -14,9 +14,6 @@ class PreferencesRepositoryImpl(
override suspend fun selectedDayDate(): String? = dao.selectedDayDate()
override suspend fun setSelectedDayDate(date: String) = dao.setSelectedDayDate(date)
override suspend fun autoUpdateEnabled(): Boolean = dao.autoUpdateEnabled() == "true"
override suspend fun autoUpdateEnabled(): Boolean = dao.autoUpdateEnabled() != "false"
override suspend fun setAutoUpdateEnabled(enabled: Boolean) = dao.setAutoUpdateEnabled(enabled.toString())
override suspend fun autoUpdateAsked(): Boolean = dao.autoUpdateAsked() == "true"
override suspend fun setAutoUpdateAsked(asked: Boolean) = dao.setAutoUpdateAsked(asked.toString())
}
@@ -11,7 +11,4 @@ interface PreferencesRepository {
suspend fun autoUpdateEnabled(): Boolean
suspend fun setAutoUpdateEnabled(enabled: Boolean)
suspend fun autoUpdateAsked(): Boolean
suspend fun setAutoUpdateAsked(asked: Boolean)
}
@@ -4,32 +4,23 @@ 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 androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
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
@@ -42,7 +33,6 @@ 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
@@ -56,32 +46,20 @@ 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.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.DateTimeFormatter
val hPadding = 16.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
val state by viewModel.uiState.collectAsState()
Column(Modifier.fillMaxSize()) {
TopAppBar(
title = { Text(state.groupName ?: "Расписание", style = MaterialTheme.typography.titleMedium) },
actions = {
if (state.isLoading && state.schedule != null) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
}
TextButton(onClick = { onSwitchGroup() }) {
Text("Сменить группу")
}
@@ -91,9 +69,18 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
},
windowInsets = WindowInsets(0.dp)
)
Column(Modifier.fillMaxSize().padding(horizontal = 0.dp, vertical = 0.dp)) {
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) {
when {
state.schedule == null && state.loadError -> {
state.isSyncing && state.schedule == null -> {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
}
}
state.schedule == null -> {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
@@ -108,15 +95,6 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
}
}
state.schedule == null -> {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
}
}
else -> {
val schedule = state.schedule!!
val weekDays = state.weekStart
@@ -132,7 +110,7 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
val dayLabel = if (isTodayDay) "Сегодня " else ""
Row(
Modifier.fillMaxWidth().padding(horizontal = hPadding),
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
@@ -147,7 +125,7 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
onSelect = viewModel::selectWeek
)
}
HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 0.dp, start = hPadding, end = hPadding))
HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 0.dp))
DayChips(
days = weekDays,
@@ -156,14 +134,14 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
onSelect = viewModel::selectDay
)
HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding))
HorizontalDivider(Modifier.padding(top=0.dp, bottom = 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),
contentPadding = PaddingValues(vertical = 8.dp)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(day.intervals) { slotIndex, slot ->
LessonSlot(slotIndex = slotIndex, slot = slot)
@@ -249,7 +227,7 @@ private fun DayChips(
todayDate: LocalDate?,
onSelect: (LocalDate) -> Unit
) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
items(days, key = { it.date.toEpochDay() }) { day ->
val selected = selectedDate == day.date
val isTodayChip = todayDate == day.date
@@ -297,93 +275,56 @@ private fun weekLabel(period: SchedulePeriod): String {
@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)
color = MaterialTheme.colorScheme.primary
)
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)"
//),
) {
slot.lessons.forEach { lesson ->
Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
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()) {
if(lesson.type != LessonType.OTHER) {
Text(
meta,
lesson.type.label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = hPadding)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
any = true
}
val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw
if (subject.isNotEmpty()) {
if(subject.isNotEmpty()) {
Text(
subject,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = hPadding)
style = MaterialTheme.typography.bodyLarge
)
any = true
}
if (!any) {
val meta = listOfNotNull(
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)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
any = true
}
if(!any) {
Text(
"-",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@@ -391,7 +332,6 @@ private fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
}
}
}
}
}
private fun dayName(dayOfWeek: DayOfWeek): String = when (dayOfWeek) {
@@ -37,7 +37,6 @@ class ScheduleViewModel(
fun selectGroup(groupId: String) {
currentGroupId = groupId
_uiState.value = _uiState.value.copy(noGroup = false, isLoading = true, loadError = false)
viewModelScope.launch {
preferencesRepository.setSelectedGroup(groupId)
val groupName = groupRepository.getGroups().firstOrNull { it.id == groupId }?.name
@@ -62,7 +61,7 @@ class ScheduleViewModel(
}
launch {
syncIfMissing(groupId)
syncIfMissingToday(groupId)
}
}
}
@@ -97,9 +96,7 @@ class ScheduleViewModel(
schedule = schedule,
weekStart = weekStart,
dayDate = dayDate,
todayDate = todayDate,
isLoading = false,
loadError = false
todayDate = todayDate
)
}
@@ -131,36 +128,30 @@ class ScheduleViewModel(
}
}
private suspend fun syncIfMissing(groupId: String) {
if (scheduleRepository.getSchedule(groupId) != null) {
_uiState.value = _uiState.value.copy(isLoading = false)
return
}
loadSchedule(groupId)
}
private suspend fun loadSchedule(groupId: String) {
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
val schedule = scheduleRepository.sync(groupId)
_uiState.value = if (schedule != null) {
_uiState.value.copy(isLoading = false, loadError = false)
} else {
_uiState.value.copy(isLoading = false, loadError = true)
}
private suspend fun syncIfMissingToday(groupId: String) {
val schedule = scheduleRepository.getSchedule(groupId)
val today = ScheduleDates.moscowToday()
val hasTodayPeriod = schedule?.periodOf(today) != null
if (hasTodayPeriod) return
_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, isLoading = false, loadError = false)
_uiState.value = _uiState.value.copy(noGroup = true)
}
}
fun refresh() {
val groupId = currentGroupId ?: return
viewModelScope.launch {
loadSchedule(groupId)
_uiState.value = _uiState.value.copy(isRefreshing = true)
scheduleRepository.sync(groupId)
_uiState.value = _uiState.value.copy(isRefreshing = false)
}
}
@@ -184,8 +175,8 @@ class ScheduleViewModel(
data class ScheduleUiState(
val noGroup: Boolean = false,
val isLoading: Boolean = false,
val loadError: Boolean = false,
val isRefreshing: Boolean = false,
val isSyncing: Boolean = false,
val schedule: Schedule? = null,
val groupName: String? = null,
val weekStart: LocalDate? = null,
@@ -50,12 +50,12 @@ fun SettingsScreen(viewModel: UpdateViewModel) {
item {
SettingsItem(
title = "Проверить наличие обновлений",
subtitle = updateStatusText(updateState, autoEnabled),
subtitle = updateStatusText(updateState),
onClick = { viewModel.checkNow() }
)
SettingsDivider()
}
if (autoEnabled && (updateState is UpdateState.Available || updateState is UpdateState.ReadyToInstall)) {
if (updateState is UpdateState.Available || updateState is UpdateState.ReadyToInstall) {
item {
SettingsItem(
title = "Установить обновление",
@@ -89,13 +89,11 @@ fun SettingsScreen(viewModel: UpdateViewModel) {
}
}
private fun updateStatusText(state: UpdateState, autoEnabled: Boolean): String = when (state) {
private fun updateStatusText(state: UpdateState): String = when (state) {
is UpdateState.Idle -> "Проверить сейчас"
is UpdateState.Checking -> "Проверка…"
is UpdateState.UpToDate -> "Установлена последняя версия"
is UpdateState.Available ->
if (autoEnabled) "Доступна версия ${state.version}"
else "Доступна версия ${state.version} — включите автообновления для установки"
is UpdateState.Available -> "Доступна версия ${state.version}"
is UpdateState.Downloading -> "Скачивание: ${state.progress?.let { "$it%" } ?: "…"}"
is UpdateState.ReadyToInstall -> "Обновление скачано, нажмите для установки"
is UpdateState.Failed -> "Ошибка: ${state.message}"
@@ -17,33 +17,18 @@ class UpdateViewModel(
val updateState: StateFlow<UpdateState> = updater.state
private val _autoEnabled = MutableStateFlow(false)
private val _autoEnabled = MutableStateFlow(true)
val autoEnabled: StateFlow<Boolean> = _autoEnabled.asStateFlow()
private val _showFirstRunDialog = MutableStateFlow(false)
val showFirstRunDialog: StateFlow<Boolean> = _showFirstRunDialog.asStateFlow()
init {
viewModelScope.launch {
_autoEnabled.value = preferencesRepository.autoUpdateEnabled()
_showFirstRunDialog.value = !preferencesRepository.autoUpdateAsked()
}
}
fun setAutoEnabled(enabled: Boolean) {
_autoEnabled.value = enabled
updater.setAutoEnabled(enabled)
if (enabled && !updater.canRequestInstalls()) {
updater.openUnknownSourcesSettings()
}
}
fun onFirstRunDecision(enable: Boolean) {
_showFirstRunDialog.value = false
viewModelScope.launch {
preferencesRepository.setAutoUpdateAsked(true)
}
setAutoEnabled(enable)
}
fun checkNow() = updater.checkForUpdate()
@@ -9,7 +9,6 @@ import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.core.content.FileProvider
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -76,7 +75,6 @@ class AppUpdater(
if (job?.isActive == true) return
val update = pendingUpdate ?: return
job = scope.launch {
if (!preferencesRepository.autoUpdateEnabled()) return@launch
runCatching { install(download(update)) }.onFailure(::onUpdateFailed)
}
}
@@ -87,12 +85,6 @@ class AppUpdater(
}
fun setAutoEnabled(enabled: Boolean) {
if (!enabled) {
job?.cancel()
if (_state.value is UpdateState.Downloading || _state.value is UpdateState.Checking) {
_state.value = UpdateState.Idle
}
}
scope.launch {
preferencesRepository.setAutoUpdateEnabled(enabled)
}
@@ -174,6 +166,7 @@ class AppUpdater(
private fun install(apk: File) {
if (!canRequestInstalls()) {
openUnknownSourcesSettings()
return
}
val uri = FileProvider.getUriForFile(
@@ -191,7 +184,6 @@ class AppUpdater(
}
private fun onUpdateFailed(error: Throwable) {
if (error is CancellationException) throw error
Log.e(TAG, "Ошибка обновления", error)
_state.value = UpdateState.Failed(error.message ?: "Неизвестная ошибка")
}
@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
-109
View File
@@ -1,109 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="100dp"
android:height="100dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M1,6L1,18L19,18L19,6"
android:strokeLineJoin="round"
android:strokeWidth="0"
android:strokeLineCap="round">
<aapt:attr name="android:fillColor">
<gradient
android:startX="1"
android:startY="18"
android:endX="19"
android:endY="6"
android:type="linear">
<item android:offset="0" android:color="#FF5581D5"/>
<item android:offset="1" android:color="#FF4063CD"/>
</gradient>
</aapt:attr>
<aapt:attr name="android:strokeColor">
<gradient
android:startX="1"
android:startY="18"
android:endX="19"
android:endY="6"
android:type="linear">
<item android:offset="0" android:color="#FF5581D5"/>
<item android:offset="1" android:color="#FF4063CD"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M19,18L1,18C12,18 19,14 19,6Z"
android:strokeLineJoin="round"
android:strokeWidth="0"
android:strokeLineCap="round">
<aapt:attr name="android:fillColor">
<gradient
android:startX="1"
android:startY="18"
android:endX="19"
android:endY="6"
android:type="linear">
<item android:offset="0" android:color="#FF6598E3"/>
<item android:offset="1" android:color="#FF74A2E6"/>
</gradient>
</aapt:attr>
<aapt:attr name="android:strokeColor">
<gradient
android:startX="1"
android:startY="18"
android:endX="19"
android:endY="6"
android:type="linear">
<item android:offset="0" android:color="#FF6598E3"/>
<item android:offset="1" android:color="#FF74A2E6"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M4,4C4,2 6,2 6,4L14,4C14,2 16,2 16,4L19,4L19,6L1,6L1,4Z"
android:strokeLineJoin="round"
android:strokeWidth="0"
android:strokeLineCap="round">
<aapt:attr name="android:fillColor">
<gradient
android:startX="1"
android:startY="2.5"
android:endX="19"
android:endY="2.5"
android:type="linear">
<item android:offset="0" android:color="#FF4260C4"/>
<item android:offset="1" android:color="#FF0E2CB0"/>
</gradient>
</aapt:attr>
<aapt:attr name="android:strokeColor">
<gradient
android:startX="1"
android:startY="2.5"
android:endX="19"
android:endY="2.5"
android:type="linear">
<item android:offset="0" android:color="#FF4260C4"/>
<item android:offset="1" android:color="#FF0E2CB0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M3,8L17,8C18,8 18,9 17,9L3,9C2,9 2,8 3,8M3,11L14,11C15,11 15,12 14,12L3,12C2,12 2,11 3,11M4,14L12,14C13,14 13,15 12,15L5,15C4,15 4,14 5,14M3,14C4,14 4,15 3,15C2,15 2,14 3,14"
android:strokeLineJoin="round"
android:strokeWidth="0"
android:strokeColor="#00000000"
android:strokeLineCap="round">
<aapt:attr name="android:fillColor">
<gradient
android:startX="2.25"
android:startY="8"
android:endX="17.75"
android:endY="8"
android:type="linear">
<item android:offset="0" android:color="#FF4260C4"/>
<item android:offset="1" android:color="#FF0E2CB0"/>
</gradient>
</aapt:attr>
</path>
</vector>
-35
View File
@@ -1,35 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100px" height="100px" viewBox="-10 -10 20 20">
<defs>
<linearGradient id="accent">
<stop offset="0%" stop-color="#4260c4" />
<stop offset="100%" stop-color="#0e2cb0" />
</linearGradient>
<linearGradient id="bg" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#5581d5" />
<stop offset="100%" stop-color="#4063cd" />
</linearGradient>
<linearGradient id="bga" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6598e3" />
<stop offset="100%" stop-color="#74a2e6" />
</linearGradient>
</defs>
<g stroke-width="0" stroke-linejoin="round" stroke-linecap="round">
<!-- <path stroke="#111" d="M -9 -4 L -9 8 L 9 8 L 9 -4"/> -->
<path stroke="url(#bg)" fill="url(#bg)" d="M -9 -4 L -9 8 L 9 8 L 9 -4"/>
<path stroke="url(#bga)" fill="url(#bga)" d="M 9 8 L -9 8 C 2 8 9 4 9 -4 Z"/>
<!-- <path stroke="#111" d="M -6 -6 C -6 -8 -4 -8 -4 -6 L 4 -6 C 4 -8 6 -8 6 -6 L 9 -6 L 9 -4 L -9 -4 L -9 -6 Z"/> -->
<g stroke="url(#accent)">
<path fill="url(#accent)" d="M -6 -6 C -6 -8 -4 -8 -4 -6 L 4 -6 C 4 -8 6 -8 6 -6 L 9 -6 L 9 -4 L -9 -4 L -9 -6 Z"/>
<path fill="url(#accent)" stroke="none" d="M -7 -2 L 7 -2 C 8 -2 8 -1 7 -1 L -7 -1 C -8 -1 -8 -2 -7 -2 M -7 1 L 4 1 C 5 1 5 2 4 2 L -7 2 C -8 2 -8 1 -7 1 M -6 4 L 2 4 C 3 4 3 5 2 5 L -5 5 C -6 5 -6 4 -5 4 M -7 4 C -6 4 -6 5 -7 5 C -8 5 -8 4 -7 4"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">ВятГУ Расписание</string>
<string name="app_name">VyatsuSchedule</string>
</resources>