Compare commits

...

5 Commits

9 changed files with 254 additions and 113 deletions
+12 -7
View File
@@ -1,5 +1,6 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Properties
@@ -27,7 +28,6 @@ android {
targetSdk = 36
versionCode = 1
versionName = "1.0"
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -76,9 +76,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
viewBinding = true
@@ -94,6 +91,16 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
base {
archivesName = "ru.sweetbread.unn-v${android.defaultConfig.versionCode}(${android.defaultConfig.versionName})"
}
dependencies {
implementation(libs.androidx.material.icons.core.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -132,8 +139,6 @@ dependencies {
implementation(libs.splitties.base)
implementation(libs.splitties.room)
implementation(libs.compose)
implementation(libs.sentry)
implementation(libs.androidx.room.runtime)
@@ -0,0 +1,50 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@Composable
fun GroupsNameText(
text: String,
modifier: Modifier = Modifier,
highlightColor: Color = MaterialTheme.colorScheme.secondaryContainer,
highlightPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
highlightShape: Shape = MaterialTheme.shapes.small,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
) {
val suffix = if (text.endsWith("-В-OUP")) "-В-OUP" else null
val mainPart = if (suffix != null) text.dropLast(6) else text
val blocks = mainPart.split('|')
FlowRow(
modifier = modifier,
horizontalArrangement = horizontalArrangement,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
blocks.forEach { block ->
Box(
modifier = Modifier
.padding(end = 2.dp)
.background(highlightColor, highlightShape)
.padding(highlightPadding)
) {
Text(text = block)
}
}
suffix?.let { Text(text = it) }
}
}
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
@@ -9,16 +9,13 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LinearProgressIndicator
@@ -39,7 +36,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -47,8 +43,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import com.kizitonwose.calendar.core.WeekDay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -63,8 +57,6 @@ import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.api.getScheduleDay
import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr
import splitties.resources.appStrArray
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@@ -75,77 +67,20 @@ import java.util.Calendar
@Composable
fun Schedule() {
var selectedDate by remember { mutableStateOf(LocalDate.now()) }
val calendarState = rememberWeekCalendarState(
startDate = LocalDate.now(),
firstVisibleWeekDate = LocalDate.now(),
firstDayOfWeek = DayOfWeek.MONDAY
)
Column {
Row(
BoundedWeekPicker(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
calendarState.firstVisibleWeek.days
.filter { it.date.dayOfWeek != DayOfWeek.SUNDAY }
.forEach { day ->
DayItem (
Modifier
.weight(1f)
.aspectRatio(1f)
.padding(2.dp),
day = day,
isSelected = day.date == selectedDate,
onClick = { selectedDate = day.date }
)
}
}
.padding(vertical = 8.dp),
selectedDate = selectedDate,
onDateSelected = { selectedDate = it }
)
ScheduleDay(date = selectedDate)
}
}
@Composable
private fun DayItem(
modifier: Modifier = Modifier,
day: WeekDay,
isSelected: Boolean,
onClick: () -> Unit
) {
val isToday = day.date == LocalDate.now()
Box(
modifier = modifier
.background(
color = if (isSelected) MaterialTheme.colorScheme.inversePrimary
else MaterialTheme.colorScheme.surfaceContainer,
shape = if (isToday) CircleShape else RectangleShape
)
.clickable(
onClick = onClick,
enabled = !isSelected
),
contentAlignment = Alignment.Center
) {
Column (
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = day.date.dayOfMonth.toString(),
fontWeight = if (isToday) FontWeight.Bold else null,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = appStrArray(R.array.short_weekdays)[day.date.dayOfWeek.value-1],
fontWeight = if (isToday) FontWeight.Bold else null,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
@Composable
fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
val scope = rememberCoroutineScope()
@@ -214,12 +149,11 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
}
val backgroundColor by animateColorAsState(
targetValue = if (ratio == 1f)
MaterialTheme.colorScheme.surfaceContainer
else if (ratio == -1f)
MaterialTheme.colorScheme.secondaryContainer
else
MaterialTheme.colorScheme.primaryContainer,
targetValue = when (ratio) {
1f -> MaterialTheme.colorScheme.surfaceContainer
-1f -> MaterialTheme.colorScheme.secondaryContainer
else -> MaterialTheme.colorScheme.primaryContainer
},
label = "backgroundTransition"
)
@@ -256,7 +190,10 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
overflow = TextOverflow.Ellipsis
)
AnimatedVisibility (expanded) {
Text(text = unit.stream)
GroupsNameText(
unit.stream,
highlightColor = MaterialTheme.colorScheme.inversePrimary
)
}
}
@@ -0,0 +1,141 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import java.util.Locale
@Composable
fun BoundedWeekPicker(
modifier: Modifier = Modifier,
selectedDate: LocalDate,
onDateSelected: (LocalDate) -> Unit,
baseDate: LocalDate = LocalDate.now()
) {
val rangeStart = remember(baseDate) {
baseDate
.with(DayOfWeek.MONDAY)
.minusWeeks(2)
}
val pageCount = 2 /* prev */ + 1 /* cur */ + 5 /* next */
fun pageIndexForDate(date: LocalDate): Int {
val weekStart = date.with(DayOfWeek.MONDAY)
return ChronoUnit.WEEKS.between(rangeStart, weekStart)
.toInt()
.coerceIn(0, pageCount - 1)
}
val initialPage = remember(selectedDate) { pageIndexForDate(selectedDate) }
val pagerState = rememberPagerState(
initialPage = initialPage,
pageCount = { pageCount }
)
LaunchedEffect(selectedDate) {
val targetPage = pageIndexForDate(selectedDate)
if (targetPage != pagerState.currentPage) {
pagerState.scrollToPage(targetPage)
}
}
HorizontalPager(
state = pagerState,
modifier = modifier,
beyondViewportPageCount = 1,
pageSpacing = 0.dp
) { page ->
val weekStart = rangeStart.plusWeeks(page.toLong())
val days = List(6) { weekStart.plusDays(it.toLong()) }
WeekPage(
days = days,
selectedDate = selectedDate,
onDateSelected = onDateSelected
)
}
}
@Composable
private fun WeekPage(
days: List<LocalDate>,
selectedDate: LocalDate,
onDateSelected: (LocalDate) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
days.forEach { date ->
DayItem(
date = date,
isSelected = date == selectedDate,
onClick = { onDateSelected(date) }
)
}
}
}
@Composable
private fun DayItem(
date: LocalDate,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
val contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
val fontWeight = if (date == LocalDate.now()) FontWeight.ExtraBold else FontWeight.Normal
Box(
modifier = modifier
.size(56.dp)
.clip(CircleShape)
.background(backgroundColor)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
style = MaterialTheme.typography.labelSmall,
color = contentColor,
fontWeight = fontWeight
)
Text(
text = date.dayOfMonth.toString(),
style = MaterialTheme.typography.bodyLarge,
color = contentColor,
fontWeight = fontWeight
)
}
}
}
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.layout
@@ -173,7 +173,7 @@ class MainActivity : ComponentActivity() {
}
) { innerPadding ->
Box(Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = "portal/blogposts") {
NavHost(navController, startDestination = "journal/schedule") {
composable("portal/blogposts") {
Blogposts()
}
+2 -2
View File
@@ -1,11 +1,11 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.1.20-2.0.0" apply false
id("com.google.devtools.ksp") version "2.3.2" apply false
}
buildscript {
+11 -1
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
@@ -23,3 +23,13 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
+18 -20
View File
@@ -1,28 +1,27 @@
[versions]
agp = "8.7.3"
calendar = "2.6.2"
agp = "9.0.1"
coilCompose = "2.7.0"
compose = "1.8.0"
coreSplashscreen = "1.0.1"
datastorePreferences = "1.1.5"
compose = "1.10.3"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.0"
desugar_jdk_libs = "2.1.5"
kotlin = "2.1.20"
coreKtx = "1.16.0"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
ktor = "2.3.12"
lifecycle = "2.8.7"
activityCompose = "1.10.1"
composeBom = "2025.04.01"
appcompat = "1.7.0"
material = "1.12.0"
kotlin = "2.3.10"
coreKtx = "1.17.0"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
ktor = "3.4.0"
lifecycle = "2.10.0"
activityCompose = "1.12.4"
composeBom = "2026.02.00"
appcompat = "1.7.1"
material = "1.13.0"
annotation = "1.9.1"
constraintlayout = "2.2.1"
activity = "1.10.1"
navigationCompose = "2.8.9"
roomRuntime = "2.7.1"
activity = "1.12.4"
navigationCompose = "2.9.7"
roomRuntime = "2.8.4"
secretsGradlePlugin = "2.0.1"
sentryAndroid = "8.12.0"
sentryAndroid = "8.32.0"
splitties = "3.0.0"
materialIconsCoreAndroid = "1.7.8"
@@ -35,7 +34,6 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
compose = { module = "com.kizitonwose.calendar:compose", version.ref = "calendar" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+2 -2
View File
@@ -1,8 +1,8 @@
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
#Sat Mar 16 18:30:45 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists