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.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Properties import java.util.Properties
@@ -27,7 +28,6 @@ android {
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -76,9 +76,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = 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 { dependencies {
implementation(libs.androidx.material.icons.core.android) implementation(libs.androidx.material.icons.core.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -132,8 +139,6 @@ dependencies {
implementation(libs.splitties.base) implementation(libs.splitties.base)
implementation(libs.splitties.room) implementation(libs.splitties.room)
implementation(libs.compose)
implementation(libs.sentry) implementation(libs.sentry)
implementation(libs.androidx.room.runtime) 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 package ru.sweetbread.unn.ui.composes
@@ -9,16 +9,13 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
@@ -39,7 +36,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow 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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex 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.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -63,8 +57,6 @@ import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.api.getScheduleDay import ru.sweetbread.unn.api.getScheduleDay
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import splitties.resources.appStrArray
import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@@ -75,77 +67,20 @@ import java.util.Calendar
@Composable @Composable
fun Schedule() { fun Schedule() {
var selectedDate by remember { mutableStateOf(LocalDate.now()) } var selectedDate by remember { mutableStateOf(LocalDate.now()) }
val calendarState = rememberWeekCalendarState(
startDate = LocalDate.now(),
firstVisibleWeekDate = LocalDate.now(),
firstDayOfWeek = DayOfWeek.MONDAY
)
Column { Column {
Row( BoundedWeekPicker(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly selectedDate = selectedDate,
) { onDateSelected = { selectedDate = it }
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 }
) )
}
}
ScheduleDay(date = selectedDate) 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 @Composable
fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) { fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -214,12 +149,11 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
} }
val backgroundColor by animateColorAsState( val backgroundColor by animateColorAsState(
targetValue = if (ratio == 1f) targetValue = when (ratio) {
MaterialTheme.colorScheme.surfaceContainer 1f -> MaterialTheme.colorScheme.surfaceContainer
else if (ratio == -1f) -1f -> MaterialTheme.colorScheme.secondaryContainer
MaterialTheme.colorScheme.secondaryContainer else -> MaterialTheme.colorScheme.primaryContainer
else },
MaterialTheme.colorScheme.primaryContainer,
label = "backgroundTransition" label = "backgroundTransition"
) )
@@ -256,7 +190,10 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
AnimatedVisibility (expanded) { 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 package ru.sweetbread.unn.ui.layout
@@ -173,7 +173,7 @@ class MainActivity : ComponentActivity() {
} }
) { innerPadding -> ) { innerPadding ->
Box(Modifier.padding(innerPadding)) { Box(Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = "portal/blogposts") { NavHost(navController, startDestination = "journal/schedule") {
composable("portal/blogposts") { composable("portal/blogposts") {
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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false
alias(libs.plugins.kotlin.compose) 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 { 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. # Project-wide Gradle settings.
# IDE (e.g. Android Studio) users: # 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, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true 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] [versions]
agp = "8.7.3" agp = "9.0.1"
calendar = "2.6.2"
coilCompose = "2.7.0" coilCompose = "2.7.0"
compose = "1.8.0" compose = "1.10.3"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.2.0"
datastorePreferences = "1.1.5" datastorePreferences = "1.2.0"
desugar_jdk_libs = "2.1.5" desugar_jdk_libs = "2.1.5"
kotlin = "2.1.20" kotlin = "2.3.10"
coreKtx = "1.16.0" coreKtx = "1.17.0"
junitVersion = "1.2.1" junitVersion = "1.3.0"
espressoCore = "3.6.1" espressoCore = "3.7.0"
ktor = "2.3.12" ktor = "3.4.0"
lifecycle = "2.8.7" lifecycle = "2.10.0"
activityCompose = "1.10.1" activityCompose = "1.12.4"
composeBom = "2025.04.01" composeBom = "2026.02.00"
appcompat = "1.7.0" appcompat = "1.7.1"
material = "1.12.0" material = "1.13.0"
annotation = "1.9.1" annotation = "1.9.1"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
activity = "1.10.1" activity = "1.12.4"
navigationCompose = "2.8.9" navigationCompose = "2.9.7"
roomRuntime = "2.7.1" roomRuntime = "2.8.4"
secretsGradlePlugin = "2.0.1" secretsGradlePlugin = "2.0.1"
sentryAndroid = "8.12.0" sentryAndroid = "8.32.0"
splitties = "3.0.0" splitties = "3.0.0"
materialIconsCoreAndroid = "1.7.8" 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-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", 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" } 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" } 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-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 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 #Sat Mar 16 18:30:45 MSK 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists