Compare commits

...
31 changed files with 404 additions and 272 deletions
+3 -6
View File
@@ -85,6 +85,9 @@ jobs:
- name: Build release APK - name: Build release APK
run: ./gradlew assembleRelease run: ./gradlew assembleRelease
env: env:
# Версия приложения — дата, сгенерированная на предыдущем шаге.
# Читается в app/build.gradle.kts как versionName (для BuildConfig).
APP_VERSION: ${{ steps.version.outputs.version_name }}
# Переменные окружения, которые читает signingConfig в app/build.gradle.kts # Переменные окружения, которые читает signingConfig в app/build.gradle.kts
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/keystore.jks ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/keystore.jks
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
@@ -112,9 +115,3 @@ jobs:
files: ${{ steps.apk.outputs.apk_path }} files: ${{ steps.apk.outputs.apk_path }}
token: ${{ secrets.GITEA_TOKEN }} token: ${{ secrets.GITEA_TOKEN }}
server_url: ${{ gitea.server_url }} server_url: ${{ gitea.server_url }}
- name: Upload APK as artifact
uses: actions/upload-artifact@v4
with:
name: vyatsu-schedule-${{ steps.version.outputs.version_name }}
path: ${{ steps.apk.outputs.apk_path }}
+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.
+3 -1
View File
@@ -17,7 +17,9 @@ android {
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" // В продакшн-сборке версию приложения подставляет CI (env APP_VERSION = дата ГГГГ.ММ.ДД).
// Локально и при отсутствии переменной — fallback "1.0".
versionName = System.getenv("APP_VERSION") ?: "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

+1 -1
View File
@@ -41,5 +41,5 @@ private class SimpleFactory<T : ViewModel>(private val create: () -> T) : ViewMo
override fun <T : ViewModel> create(modelClass: Class<T>): T = create() as T override fun <T : ViewModel> create(modelClass: Class<T>): T = create() as T
} }
private fun scheduleFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { ScheduleViewModel(c.scheduleRepository, c.preferencesRepository) } private fun scheduleFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { ScheduleViewModel(c.scheduleRepository, c.preferencesRepository, c.groupRepository) }
private fun groupPickerFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { GroupPickerViewModel(c.groupRepository) } private fun groupPickerFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { GroupPickerViewModel(c.groupRepository) }
@@ -38,7 +38,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
private val svgBuilder = StringBuilder() private val svgBuilder = StringBuilder()
private val currentPath = StringBuilder() private val currentPath = StringBuilder()
private val tableSvg = StringBuilder() val tableSvg = StringBuilder()
private val currentSubPathLines = mutableListOf<PdfLine>() private val currentSubPathLines = mutableListOf<PdfLine>()
@@ -52,7 +52,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
val width = mediaBox.width val width = mediaBox.width
val height = mediaBox.height val height = mediaBox.height
return """ return """
<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style='font-size: 50%;'>
<g>${svgBuilder}</g> <g>${svgBuilder}</g>
<g>${tableSvg}</g> <g>${tableSvg}</g>
</svg> </svg>
@@ -211,7 +211,7 @@ class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
val bottom = ys[ii+1] val bottom = ys[ii+1]
cells.add(TableCell(left, top, right-left, bottom-top)) cells.add(TableCell(left, top, right-left, bottom-top))
tableSvg.append("<rect fill='${if (i % 2 == 0) "#f" else "#0"}${if (ii % 2 == 0) "f0" else "00"}' x='$left' y='$top' width='${right-left}' height='${bottom-top}'/>\n") tableSvg.append("<rect fill='${if (i % 2 == 0) "#ff" else "#9e"}${if (ii % 2 == 0) "ff9e" else "9e9e"}' x='$left' y='$top' width='${right-left}' height='${bottom-top}'/>\n")
} }
columns.add(TableColumn(left, right-left, cells)) columns.add(TableColumn(left, right-left, cells))
} }
@@ -2,6 +2,7 @@ package agzam.vyatsu.data.remote.pdf
import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.Lesson import agzam.vyatsu.domain.model.Lesson
import agzam.vyatsu.domain.model.LessonType
import agzam.vyatsu.domain.model.TimeSlot import agzam.vyatsu.domain.model.TimeSlot
import android.util.Log import android.util.Log
import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.pdmodel.PDDocument
@@ -25,6 +26,10 @@ object PdfScheduleParser {
private fun timeFrom(timeString: String?): LocalTime? { return runCatching { LocalTime.parse(timeString, TIME_FORMAT) }.getOrNull() } private fun timeFrom(timeString: String?): LocalTime? { return runCatching { LocalTime.parse(timeString, TIME_FORMAT) }.getOrNull() }
private fun dateFrom(dateString: String?): LocalDate? { return runCatching { LocalDate.parse(dateString, DATE_FORMAT) }.getOrNull() } private fun dateFrom(dateString: String?): LocalDate? { return runCatching { LocalDate.parse(dateString, DATE_FORMAT) }.getOrNull() }
private val ROOM_REGEX = Regex("""(?<=^|\s)\d{1,2}-\d{3}[_a-zA-Zа-яА-Я]?(?=\s|$)""")
private val LESSON_TYPE_REGEX = Regex(LessonType.entries.joinToString(separator = "|") { Regex.escape(it.label) }.let { """(?<=^|\s)($it)(?=\s|$)""" } )
private val SUBGROUP_REGEX = Regex("""\d{2}\sподгруппа""", RegexOption.IGNORE_CASE)
fun parse(pdfBytes: ByteArray): List<Day> { fun parse(pdfBytes: ByteArray): List<Day> {
Log.d(TAG, "Parsing ${pdfBytes.size} bytes") Log.d(TAG, "Parsing ${pdfBytes.size} bytes")
val doc = try { val doc = try {
@@ -35,6 +40,8 @@ object PdfScheduleParser {
} }
val days = mutableListOf<Day>() val days = mutableListOf<Day>()
var group: String? = null
try { try {
for (pageIndex in 0 until doc.numberOfPages) { for (pageIndex in 0 until doc.numberOfPages) {
Log.v(TAG, "============ Page $pageIndex ============") Log.v(TAG, "============ Page $pageIndex ============")
@@ -45,12 +52,6 @@ object PdfScheduleParser {
svg.processPage(page) svg.processPage(page)
val cols = svg.asTable() val cols = svg.asTable()
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
Log.v(SVG_TAG, "====")
for(line in svg.asSvg().split("\n")) {
Log.v(SVG_TAG, line)
}
if(cols.size != 3) throw RuntimeException("Found columns: ${cols.size}/3") if(cols.size != 3) throw RuntimeException("Found columns: ${cols.size}/3")
// Парсинг текста // Парсинг текста
@@ -60,6 +61,11 @@ object PdfScheduleParser {
textStripper.getText(doc) textStripper.getText(doc)
val texts = textStripper.blocks val texts = textStripper.blocks
for(text in texts) {
svg.tableSvg.appendLine("<text x='${text.x}' y='${text.y}'>${text.text}</text>")
svg.tableSvg.appendLine("<circle cx='${text.x}' cy='${text.y}' r='1' fill='red'/>")
}
val daysColumn = cols[0] // Колонка "День", имеет формат "<день> dd.mm.yy" val daysColumn = cols[0] // Колонка "День", имеет формат "<день> dd.mm.yy"
val intervalsColumn = cols[1] // Колонка "Интервал", имеет формат "HH:mm-HH:mm" val intervalsColumn = cols[1] // Колонка "Интервал", имеет формат "HH:mm-HH:mm"
val lessonsColumn = cols[2] // Колонка с инофрмацией о паре val lessonsColumn = cols[2] // Колонка с инофрмацией о паре
@@ -100,10 +106,17 @@ object PdfScheduleParser {
} }
} }
for (dayCell in daysColumn.cells) { for (dayCell in daysColumn.cells) {
val day = content(dayCell).split(' ') val day = content(dayCell).split(' ')
if(day.size != 2) continue // XXX: добавить защиту от заголовка и выбрасывать исключения вместо пропуска? if(day.size == 1 && day.first() == "День") {
val groupCell = select(lessonsColumn, dayCell).firstOrNull() ?: continue
group = content(groupCell)
continue
}
if(day.size != 2) {
Log.w(TAG, "Day data is not in format <day> <date>: $day")
continue // XXX: добавить защиту от заголовка и выбрасывать исключения вместо пропуска?
}
val date = dateFrom(day[1]) ?: continue val date = dateFrom(day[1]) ?: continue
val intervals = mutableListOf<TimeSlot>() val intervals = mutableListOf<TimeSlot>()
@@ -124,7 +137,55 @@ object PdfScheduleParser {
val content = content(lessonCell) val content = content(lessonCell)
Log.v(TAG, "- $content") Log.v(TAG, "- $content")
// TODO: парсинг контента лекции // TODO: парсинг контента лекции
lessons.add(Lesson(raw=content))
// Поиск кабинета в строке, ибо он последний, если их несколько - то скорее всего разделение на подгруппы
var found = 0
var currentStart = 0
for (roomMatch in ROOM_REGEX.findAll(content)) {
found++
val raw = content.substring(currentStart, roomMatch.range.last + 1)
currentStart = roomMatch.range.last + 1
// Предполагаемый формат: [группа], [XX подгруппа] <предмет> <тип> [id + тип] <ФИО> <кабинет>
val room = roomMatch.value
val typeMatch = LESSON_TYPE_REGEX.findAll(raw).toList()
if(typeMatch.isEmpty()) {
lessons.add(Lesson(
raw=raw,
room=room,
))
continue
}
var subject = raw.substring(0, typeMatch.first().range.first).trim() // TODO: сейчас [группа], [XX подгруппа] <предмет> - можно достать подгруппу
val type = LessonType.fromRaw(typeMatch.first().value.trim())
val teacher = raw.substring(typeMatch.last().range.last+1, raw.length - room.length).trim()
var subgroup: String? = null
Log.d(TAG, "$subject $group")
if(group != null && subject.startsWith("$group,")) {
subject = subject.substring(group.length + 1).trim()
val subgroupMatch = SUBGROUP_REGEX.find(subject)
if(subgroupMatch != null) {
subgroup = subgroupMatch.value.trim()
subject = subject.removeRange(subgroupMatch.range).trim()
}
}
lessons.add(Lesson(
raw=raw,
subject=subject,
room=room,
type=type,
teacher=teacher,
subgroup=subgroup
))
}
if(found == 0) lessons.add(Lesson(raw=content))
} }
intervals.add(TimeSlot(start = start, end = end, lessons = lessons)) intervals.add(TimeSlot(start = start, end = end, lessons = lessons))
} }
@@ -133,6 +194,18 @@ object PdfScheduleParser {
for (text in texts.filter { it.rotation != 0f }) { for (text in texts.filter { it.rotation != 0f }) {
Log.v(TAG, "${text.x.roundToInt()}\t${text.y.roundToInt()}\t${text.text}") Log.v(TAG, "${text.x.roundToInt()}\t${text.y.roundToInt()}\t${text.text}")
} }
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
Log.v(SVG_TAG, "====")
for(line in svg.asSvg().split("\n")) {
if(line.length > 250) {
Log.v(SVG_TAG, "length: " + line.length)
Log.v(SVG_TAG + "-" + (pageIndex+1), "length: " + line.length)
continue
}
Log.v(SVG_TAG, line)
Log.v(SVG_TAG + "-" + (pageIndex+1), line)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Parsing error", e) Log.e(TAG, "Parsing error", e)
@@ -7,6 +7,7 @@ import com.tom_roush.pdfbox.text.TextPosition
* Парсер блоков текста из PDF * Парсер блоков текста из PDF
* @author Agzam * @author Agzam
*/ */
class PdfTextParser : PDFTextStripper() { class PdfTextParser : PDFTextStripper() {
data class TextBlock( data class TextBlock(
@@ -25,9 +26,32 @@ class PdfTextParser : PDFTextStripper() {
} }
override fun writeString(string: String?, textPositions: MutableList<TextPosition>?) { override fun writeString(string: String?, textPositions: MutableList<TextPosition>?) {
if (!string.isNullOrBlank() && !textPositions.isNullOrEmpty()) { if (textPositions.isNullOrEmpty()) return
val first = textPositions.first()
val last = textPositions.last() val currentWordChars = mutableListOf<TextPosition>()
for (tp in textPositions) {
val unicode = tp.unicode
if (unicode == " " || unicode == "\u00A0" || unicode == "\t") {
addWordBlock(currentWordChars)
currentWordChars.clear()
} else {
currentWordChars.add(tp)
}
}
addWordBlock(currentWordChars)
}
private fun addWordBlock(wordChars: List<TextPosition>) {
if (wordChars.isEmpty()) return
val first = wordChars.first()
val last = wordChars.last()
val wordText = wordChars.joinToString("") { it.unicode }.trim()
if (wordText.isEmpty()) return
val matrix = first.textMatrix val matrix = first.textMatrix
val rotation = first.dir val rotation = first.dir
@@ -36,10 +60,17 @@ class PdfTextParser : PDFTextStripper() {
val x = matrix.translateX val x = matrix.translateX
val y = matrix.translateY - first.heightDir val y = matrix.translateY - first.heightDir
val height = first.heightDir val height = first.heightDir
blocks.add(TextBlock(string.trim(), x, currentPage.mediaBox.height - y - height, totalWidth, height, rotation)) blocks.add(
} TextBlock(
text = wordText,
x = x,
y = currentPage.mediaBox.height - y - height,
width = totalWidth,
height = height,
rotation = rotation
)
)
} }
} }
@@ -157,6 +157,7 @@ class ScheduleRepositoryImpl(
val lessonsByDay = lessonEntities.groupBy { it.dayId } val lessonsByDay = lessonEntities.groupBy { it.dayId }
val days = dayEntities val days = dayEntities
.sortedBy { it.date } .sortedBy { it.date }
.distinctBy { it.date }
.map { day -> .map { day ->
val lessons = lessonsByDay[day.id].orEmpty() val lessons = lessonsByDay[day.id].orEmpty()
.groupBy { it.slotIndex } .groupBy { it.slotIndex }
@@ -4,12 +4,14 @@ import agzam.vyatsu.ui.info.BuildingsScreen
import agzam.vyatsu.ui.info.CallsScreen import agzam.vyatsu.ui.info.CallsScreen
import agzam.vyatsu.ui.schedule.ScheduleScreen import agzam.vyatsu.ui.schedule.ScheduleScreen
import agzam.vyatsu.ui.schedule.ScheduleViewModel import agzam.vyatsu.ui.schedule.ScheduleViewModel
import agzam.vyatsu.ui.settings.SettingsScreen
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
@@ -25,7 +27,8 @@ import androidx.compose.ui.Modifier
private enum class HomeTab(val label: String) { private enum class HomeTab(val label: String) {
SCHEDULE("Расписание"), SCHEDULE("Расписание"),
CALLS("Звонки"), CALLS("Звонки"),
BUILDINGS("Корпуса") BUILDINGS("Корпуса"),
SETTINGS("Настройки")
} }
@Composable @Composable
@@ -48,6 +51,7 @@ fun HomeScreen(
HomeTab.SCHEDULE -> Icons.Default.DateRange HomeTab.SCHEDULE -> Icons.Default.DateRange
HomeTab.CALLS -> Icons.Default.Info HomeTab.CALLS -> Icons.Default.Info
HomeTab.BUILDINGS -> Icons.Default.Home HomeTab.BUILDINGS -> Icons.Default.Home
HomeTab.SETTINGS -> Icons.Default.Settings
}, },
contentDescription = t.label contentDescription = t.label
) )
@@ -67,6 +71,7 @@ fun HomeScreen(
HomeTab.SCHEDULE -> ScheduleScreen(viewModel = scheduleViewModel, onSwitchGroup = onSwitchGroup) HomeTab.SCHEDULE -> ScheduleScreen(viewModel = scheduleViewModel, onSwitchGroup = onSwitchGroup)
HomeTab.CALLS -> CallsScreen() HomeTab.CALLS -> CallsScreen()
HomeTab.BUILDINGS -> BuildingsScreen() HomeTab.BUILDINGS -> BuildingsScreen()
HomeTab.SETTINGS -> SettingsScreen()
} }
} }
} }
@@ -1,9 +1,12 @@
package agzam.vyatsu.ui.schedule package agzam.vyatsu.ui.schedule
import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.LessonType
import agzam.vyatsu.domain.model.SchedulePeriod import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.domain.model.TimeSlot import agzam.vyatsu.domain.model.TimeSlot
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -12,14 +15,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
@@ -33,8 +40,12 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
@@ -47,7 +58,7 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
TopAppBar( TopAppBar(
title = { Text("Расписание") }, title = { Text(state.groupName ?: "Расписание", style = MaterialTheme.typography.titleMedium) },
actions = { actions = {
TextButton(onClick = { onSwitchGroup() }) { TextButton(onClick = { onSwitchGroup() }) {
Text("Сменить группу") Text("Сменить группу")
@@ -56,7 +67,7 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить") Icon(Icons.Default.Refresh, contentDescription = "Обновить")
} }
}, },
windowInsets = WindowInsets.systemBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Top) windowInsets = WindowInsets(0.dp)
) )
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) { Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) {
when { when {
@@ -98,31 +109,23 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
val isTodayDay = day.date == state.todayDate val isTodayDay = day.date == state.todayDate
val dayLabel = if (isTodayDay) "Сегодня " else "" val dayLabel = if (isTodayDay) "Сегодня " else ""
Text(
"$dayLabel ${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}",
style = MaterialTheme.typography.titleMedium
)
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) verticalAlignment = Alignment.CenterVertically
) { ) {
TextButton(onClick = { viewModel.clearSelectedPeriod() }) { Text(
Text("Очистить эту неделю") "$dayLabel${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}",
} style = MaterialTheme.typography.titleMedium,
TextButton(onClick = { viewModel.clearAll() }) { modifier = Modifier.weight(1f)
Text("Очистить всё") )
} WeekSelector(
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
WeekChips(
periods = schedule.periods, periods = schedule.periods,
selectedWeekStart = state.weekStart, selectedWeekStart = state.weekStart,
todayWeekStart = state.todayDate?.let { schedule.periodOf(it)?.weekStart }, todayWeekStart = state.todayDate?.let { schedule.periodOf(it)?.weekStart },
onSelect = viewModel::selectWeek onSelect = viewModel::selectWeek
) )
}
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 0.dp))
DayChips( DayChips(
days = weekDays, days = weekDays,
@@ -131,7 +134,7 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
onSelect = viewModel::selectDay onSelect = viewModel::selectDay
) )
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(top=0.dp, bottom = 8.dp))
if (day.intervals.all { it.lessons.isEmpty() }) { if (day.intervals.all { it.lessons.isEmpty() }) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
@@ -152,42 +155,70 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun WeekChips( private fun WeekSelector(
periods: List<SchedulePeriod>, periods: List<SchedulePeriod>,
selectedWeekStart: LocalDate?, selectedWeekStart: LocalDate?,
todayWeekStart: LocalDate?, todayWeekStart: LocalDate?,
onSelect: (LocalDate) -> Unit onSelect: (LocalDate) -> Unit
) { ) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { val selected = periods.firstOrNull { it.weekStart == selectedWeekStart }
items(periods, key = { it.weekStart.toEpochDay() }) { period -> var expanded by remember { mutableStateOf(false) }
val selected = selectedWeekStart == period.weekStart
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 val isTodayWeek = todayWeekStart == period.weekStart
FilterChip( DropdownMenuItem(
selected = selected, text = {
onClick = { onSelect(period.weekStart) },
label = {
Text( Text(
weekLabel(period), weekLabel(period),
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.bodyLarge
) )
}, },
modifier = Modifier.padding(vertical = 2.dp), trailingIcon = if (isTodayWeek) {
border = if (isTodayWeek && !selected) { {
FilterChipDefaults.filterChipBorder( Text(
borderColor = MaterialTheme.colorScheme.primary, "сегодня",
borderWidth = 1.dp, style = MaterialTheme.typography.labelMedium,
enabled = true, color = MaterialTheme.colorScheme.primary
selected = false
) )
}
} else { } else {
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) null
}, },
shape = RoundedCornerShape(8.dp) onClick = {
expanded = false
onSelect(period.weekStart)
}
) )
} }
} }
} }
}
@Composable @Composable
private fun DayChips( private fun DayChips(
@@ -255,22 +286,48 @@ private fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
} else { } else {
slot.lessons.forEach { lesson -> slot.lessons.forEach { lesson ->
Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
var any = false
if(lesson.type != LessonType.OTHER) {
Text( Text(
lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw,
style = MaterialTheme.typography.bodyLarge
)
Text(
listOfNotNull(
lesson.type.label, lesson.type.label,
lesson.subgroup,
lesson.teacher,
lesson.room
)
.filter { it.isNotBlank() }
.joinToString(" · "),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
any = true
}
val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw
if(subject.isNotEmpty()) {
Text(
subject,
style = MaterialTheme.typography.bodyLarge
)
any = true
}
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
)
any = true
}
if(!any) {
Text(
"-",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
@@ -2,6 +2,7 @@ package agzam.vyatsu.ui.schedule
import agzam.vyatsu.domain.ScheduleDates import agzam.vyatsu.domain.ScheduleDates
import agzam.vyatsu.domain.model.Schedule import agzam.vyatsu.domain.model.Schedule
import agzam.vyatsu.domain.repository.GroupRepository
import agzam.vyatsu.domain.repository.PreferencesRepository import agzam.vyatsu.domain.repository.PreferencesRepository
import agzam.vyatsu.domain.repository.ScheduleRepository import agzam.vyatsu.domain.repository.ScheduleRepository
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -14,7 +15,8 @@ import java.time.LocalDate
class ScheduleViewModel( class ScheduleViewModel(
private val scheduleRepository: ScheduleRepository, private val scheduleRepository: ScheduleRepository,
private val preferencesRepository: PreferencesRepository private val preferencesRepository: PreferencesRepository,
private val groupRepository: GroupRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(ScheduleUiState()) private val _uiState = MutableStateFlow(ScheduleUiState())
@@ -28,7 +30,7 @@ class ScheduleViewModel(
if (groupId != null) { if (groupId != null) {
selectGroup(groupId) selectGroup(groupId)
} else { } else {
_uiState.value = _uiState.value.copy(noGroup = true) _uiState.value = _uiState.value.copy(noGroup = true, groupName = null)
} }
} }
} }
@@ -37,6 +39,7 @@ class ScheduleViewModel(
currentGroupId = groupId currentGroupId = groupId
viewModelScope.launch { viewModelScope.launch {
preferencesRepository.setSelectedGroup(groupId) preferencesRepository.setSelectedGroup(groupId)
val groupName = groupRepository.getGroups().firstOrNull { it.id == groupId }?.name
val savedWeekStart = runCatching { val savedWeekStart = runCatching {
preferencesRepository.selectedWeekStart()?.let(LocalDate::parse) preferencesRepository.selectedWeekStart()?.let(LocalDate::parse)
@@ -46,6 +49,7 @@ class ScheduleViewModel(
}.getOrNull() }.getOrNull()
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
noGroup = false, noGroup = false,
groupName = groupName,
weekStart = savedWeekStart, weekStart = savedWeekStart,
dayDate = savedDayDate dayDate = savedDayDate
) )
@@ -174,6 +178,7 @@ data class ScheduleUiState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val isSyncing: Boolean = false, val isSyncing: Boolean = false,
val schedule: Schedule? = null, val schedule: Schedule? = null,
val groupName: String? = null,
val weekStart: LocalDate? = null, val weekStart: LocalDate? = null,
val dayDate: LocalDate? = null, val dayDate: LocalDate? = null,
val todayDate: LocalDate? = null val todayDate: LocalDate? = null
@@ -0,0 +1,53 @@
package agzam.vyatsu.ui.settings
import agzam.vyatsu.BuildConfig
import androidx.compose.foundation.layout.Column
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.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun SettingsScreen() {
Column(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(24.dp))
Text(
"Настройки",
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.weight(1f))
Text(
"Здесь появятся настройки\n(автоматические обновления и др.)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(Modifier.weight(1f))
Text(
"Версия ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
)
}
}
@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector
android:width="108dp"
android:height="108dp" android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path android:fillColor="#3DDC84"
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M9,0L9,108" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:pathData="M19,0L19,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M29,0L29,108" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:pathData="M39,0L39,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M49,0L49,108" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:pathData="M59,0L59,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M69,0L69,108" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:pathData="M79,0L79,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M89,0L89,108" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:pathData="M99,0L99,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,9L108,9" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:pathData="M0,19L108,19" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,29L108,29" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" 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" />
</vector> </vector>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> <monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> <monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </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