Compare commits

...
7 changed files with 149 additions and 30 deletions
+2 -2
View File
@@ -74,9 +74,9 @@ jobs:
SUFFIX=$((SUFFIX + 1)) SUFFIX=$((SUFFIX + 1))
TAG="v${BASE}.${SUFFIX}" TAG="v${BASE}.${SUFFIX}"
done done
echo "version_name=$BASE" >> "$GITHUB_OUTPUT" echo "version_name=$TAG" >> "$GITHUB_OUTPUT"
echo "version_tag=$TAG" >> "$GITHUB_OUTPUT" echo "version_tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Generated version: $BASE (tag: $TAG)" echo "Generated version: $BASE"
- name: Restore keystore from secret - name: Restore keystore from secret
# Ключ подписи передаётся как base64-секрет и декодируется в файл. # Ключ подписи передаётся как base64-секрет и декодируется в файл.
+2 -1
View File
@@ -40,7 +40,8 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
+20 -1
View File
@@ -18,4 +18,23 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
# Игнорировать отсутствие класса декодера JP2 в PdfBox-Android
-dontwarn com.gemalto.jp2.JP2Decoder
# На всякий случай, если PdfBox ссылается на другие отсутствующие библиотеки (например, ICU для текста)
-dontwarn com.tom_roush.pdfbox.**
# Запретить R8 удалять и переименовывать внутренности PdfBox-Android
-keep class com.tom_roush.pdfbox.** { *; }
-keep interface com.tom_roush.pdfbox.** { *; }
# Moshi удаляется proguard
-keepnames @com.squareup.moshi.JsonClass class *
-keep class **JsonAdapter {
public <init>(...);
<fields>;
}
-keepclassmembers @com.squareup.moshi.JsonClass class * {
synthetic <init>(...);
}
@@ -108,9 +108,13 @@ 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 == 1) {
Log.v(TAG, "day.first(): ${day.first()}")
}
if(day.size == 1 && day.first() == "День") { if(day.size == 1 && day.first() == "День") {
val groupCell = select(lessonsColumn, dayCell).firstOrNull() ?: continue val groupCell = select(lessonsColumn, dayCell).firstOrNull() ?: continue
group = content(groupCell) group = content(groupCell)
Log.v(TAG, "Group: $group")
continue continue
} }
if(day.size != 2) { if(day.size != 2) {
@@ -164,7 +168,7 @@ object PdfScheduleParser {
val teacher = raw.substring(typeMatch.last().range.last+1, raw.length - room.length).trim() val teacher = raw.substring(typeMatch.last().range.last+1, raw.length - room.length).trim()
var subgroup: String? = null var subgroup: String? = null
Log.d(TAG, "$subject $group") Log.d(TAG, "$subject $group")
if(group != null && subject.startsWith("$group,")) { if(group != null && (subject.startsWith("$group,") || subject.startsWith("$group "))) {
subject = subject.substring(group.length + 1).trim() subject = subject.substring(group.length + 1).trim()
val subgroupMatch = SUBGROUP_REGEX.find(subject) val subgroupMatch = SUBGROUP_REGEX.find(subject)
if(subgroupMatch != null) { if(subgroupMatch != null) {
@@ -197,6 +201,7 @@ object PdfScheduleParser {
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/ /** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
Log.v(SVG_TAG, "====") Log.v(SVG_TAG, "====")
Log.v(SVG_TAG+ "-" + (pageIndex+1), "====")
for(line in svg.asSvg().split("\n")) { for(line in svg.asSvg().split("\n")) {
if(line.length > 250) { if(line.length > 250) {
Log.v(SVG_TAG, "length: " + line.length) Log.v(SVG_TAG, "length: " + line.length)
@@ -54,17 +54,5 @@ fun BuildingsScreen() {
} }
HorizontalDivider(Modifier.padding(horizontal = 16.dp)) HorizontalDivider(Modifier.padding(horizontal = 16.dp))
} }
item {
Spacer(Modifier.height(24.dp))
Text(
"Версия ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
}
} }
} }
@@ -25,9 +25,15 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.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.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -48,20 +54,27 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
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.draw.clip
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
val hPadding = 16.dp val hPadding = 16.dp
@@ -128,6 +141,32 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
if (day == null) { if (day == null) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else { } else {
val schedule = state.schedule!!
val weekDays = state.weekStart
?.let { ws -> schedule.periods.firstOrNull { it.weekStart == ws } }
?.let { schedule.daysOf(it) }
.orEmpty()
val selectedDayIndex = weekDays.indexOfFirst { it.date == state.dayDate }.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
LaunchedEffect(state.dayDate) {
val targetPage = weekDays.indexOfFirst { it.date == state.dayDate }
if (targetPage != -1 && targetPage != pagerState.currentPage) {
pagerState.scrollToPage(targetPage) // Мгновенное переключение без анимации
}
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
if (pagerState.isScrollInProgress && page in weekDays.indices) {
viewModel.selectDay(weekDays[page].date)
}
}
}
val isTodayDay = day.date == state.todayDate val isTodayDay = day.date == state.todayDate
val dayLabel = if (isTodayDay) "Сегодня " else "" val dayLabel = if (isTodayDay) "Сегодня " else ""
@@ -153,20 +192,31 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
days = weekDays, days = weekDays,
selectedDate = state.dayDate, selectedDate = state.dayDate,
todayDate = state.todayDate, todayDate = state.todayDate,
onSelect = viewModel::selectDay onSelect = viewModel::selectDay,
pagerState = pagerState
) )
HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding)) HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding))
if (day.intervals.all { it.lessons.isEmpty() }) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page ->
} else { val day = weekDays.getOrNull(page)
LazyColumn(
modifier = Modifier.weight(1f), if (day == null || day.intervals.all { it.lessons.isEmpty() }) {
verticalArrangement = Arrangement.spacedBy(8.dp), Box(
contentPadding = PaddingValues(vertical = 8.dp) modifier = Modifier.fillMaxSize(),
) { contentAlignment = Alignment.Center
itemsIndexed(day.intervals) { slotIndex, slot -> ) {
LessonSlot(slotIndex = slotIndex, slot = slot) Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
itemsIndexed(day.intervals) { slotIndex, slot ->
LessonSlot(slotIndex = slotIndex, slot = slot)
}
} }
} }
} }
@@ -242,14 +292,70 @@ private fun WeekSelector(
} }
} }
private fun getVisiblePercentage(item: LazyListItemInfo, layoutInfo: LazyListLayoutInfo): Float {
val viewportStart = layoutInfo.viewportStartOffset
val viewportEnd = layoutInfo.viewportEndOffset
val itemStart = item.offset
val itemEnd = item.offset + item.size
val visibleStart = max(itemStart, viewportStart)
val visibleEnd = min(itemEnd, viewportEnd)
val visibleSize = visibleEnd - visibleStart
return if (visibleSize <= 0) 0f else visibleSize.toFloat() / item.size
}
@Composable @Composable
private fun DayChips( private fun DayChips(
days: List<Day>, days: List<Day>,
selectedDate: LocalDate?, selectedDate: LocalDate?,
todayDate: LocalDate?, todayDate: LocalDate?,
pagerState: PagerState,
onSelect: (LocalDate) -> Unit onSelect: (LocalDate) -> Unit
) { ) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) { val listState = rememberLazyListState()
val density = LocalDensity.current
val paddingPx = with(density) { hPadding.toPx() }
LaunchedEffect(pagerState, days) {
snapshotFlow {
pagerState.currentPage
}.collectLatest { targetPage ->
if (targetPage in days.indices) {
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
val targetItem = visibleItems.firstOrNull { it.index == targetPage }
if (targetItem != null) {
val visiblePercentage = getVisiblePercentage(targetItem, layoutInfo)
val minVisibleThreshold = 0.9f
if (visiblePercentage < minVisibleThreshold) {
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
val halfViewport = viewPortWidth / 2f
val centerOffset = (halfViewport - (targetItem.size / 2f) - paddingPx).toInt()
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
}
} else {
val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
val halfViewport = viewPortWidth / 2f
val averageSize = visibleItems.map { it.size }.average().toInt().coerceAtLeast(150)
val centerOffset = (halfViewport - (averageSize / 2f) - paddingPx).toInt()
listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset)
}
}
}
}
LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
items(days, key = { it.date.toEpochDay() }) { day -> items(days, key = { it.date.toEpochDay() }) { day ->
val selected = selectedDate == day.date val selected = selectedDate == day.date
val isTodayChip = todayDate == day.date val isTodayChip = todayDate == day.date
@@ -118,7 +118,7 @@ class AppUpdater(
} }
val remoteVersion = release.tagName?.trimStart('v', 'V') val remoteVersion = release.tagName?.trimStart('v', 'V')
if (remoteVersion == null || !isNewerVersion(remoteVersion, BuildConfig.VERSION_NAME)) { if (remoteVersion == null || !isNewerVersion(remoteVersion, BuildConfig.VERSION_NAME.trimStart('v', 'V'))) {
_state.value = UpdateState.UpToDate _state.value = UpdateState.UpToDate
return null return null
} }