Compare commits

...

5 Commits

Author SHA1 Message Date
7f613a2106
impr: show images in blogposts 2025-04-24 20:56:54 +03:00
99effd1e52
deps: update 2025-04-24 20:56:54 +03:00
764eedc837
impr: add animation 2025-04-24 20:56:53 +03:00
90d1f69f0d
impr: LoginActivity.kt
- Style changed
- Added loading indicator
- Added login/password validation
2025-04-24 20:56:53 +03:00
7bfcb3d3c9
feat: add marked divider 2025-04-24 20:56:53 +03:00
7 changed files with 302 additions and 85 deletions

View File

@ -1,9 +1,15 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.kotlin.compose)
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
id("com.google.devtools.ksp")
}
@ -14,12 +20,12 @@ secrets {
android {
namespace = "ru.sweetbread.unn"
compileSdk = 34
compileSdk = 36
defaultConfig {
applicationId = "ru.sweetbread.unn"
minSdk = 26
targetSdk = 34
targetSdk = 36
versionCode = 1
versionName = "1.0"
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
@ -28,6 +34,12 @@ android {
vectorDrawables {
useSupportLibrary = true
}
// javaCompileOptions {
// annotationProcessorOptions {
// arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
// }
// }
}
buildTypes {
@ -72,6 +84,7 @@ android {
}
dependencies {
implementation(libs.androidx.material.icons.core.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.androidx.core.ktx)

View File

@ -5,8 +5,12 @@
package ru.sweetbread.unn.ui.composes
import android.graphics.drawable.Drawable
import android.text.Html
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.util.Log
import android.widget.TextView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -43,11 +47,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.ImageLoader
import coil.compose.AsyncImage
import com.google.android.material.textview.MaterialTextView
import coil.request.ImageRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -196,8 +200,10 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
val linkColor = MaterialTheme.colorScheme.primary.toArgb()
AndroidView(
modifier = Modifier,
factory = {
MaterialTextView(it).apply {
TextView(it).apply {
movementMethod = LinkMovementMethod.getInstance()
autoLinkMask = Linkify.WEB_URLS
linksClickable = true
setTextColor(textColor)
@ -206,7 +212,12 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
},
update = {
it.maxLines = if (extended) Int.MAX_VALUE else 5
it.text = HtmlCompat.fromHtml(html, 0)
it.text = Html.fromHtml(
html,
Html.FROM_HTML_MODE_LEGACY,
CoilImageGetter(it),
null
)
}
)
@ -257,4 +268,77 @@ fun PostItemPreview() {
)
}
}
}
class CoilImageGetter(
private val textView: TextView,
private val maxImageWidth: Int = textView.width
) : Html.ImageGetter {
override fun getDrawable(source: String): Drawable {
val urlDrawable = UrlDrawable()
if (maxImageWidth <= 0)
textView.post { updateImage(source, urlDrawable, textView.width) }
else
updateImage(source, urlDrawable, maxImageWidth)
return urlDrawable
}
private fun updateImage(source: String, urlDrawable: UrlDrawable, maxWidth: Int) {
val imageLoader = ImageLoader.Builder(textView.context)
.build()
val request = ImageRequest.Builder(textView.context)
.data(source)
.target { drawable ->
val (scaledWidth, scaledHeight) = calculateScaledSize(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
maxWidth
)
drawable.setBounds(0, 0, scaledWidth, scaledHeight)
urlDrawable.drawable = drawable
urlDrawable.setBounds(0, 0, scaledWidth, scaledHeight)
textView.text = textView.text
}
.build()
imageLoader.enqueue(request)
}
private fun calculateScaledSize(
originalWidth: Int,
originalHeight: Int,
maxWidth: Int
): Pair<Int, Int> {
if (originalWidth <= maxWidth)
return Pair(originalWidth, originalHeight)
val ratio = maxWidth.toFloat() / originalWidth.toFloat()
return Pair(
maxWidth,
(originalHeight * ratio).toInt()
)
}
}
class UrlDrawable() : Drawable() {
var drawable: Drawable? = null
set(value) {
field = value
invalidateSelf()
}
override fun draw(canvas: android.graphics.Canvas) {
drawable?.draw(canvas)
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {}
override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
}

View File

@ -1,7 +1,14 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.composes
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -10,6 +17,7 @@ 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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -23,6 +31,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -31,15 +40,19 @@ 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.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
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
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.sweetbread.unn.Auditorium
import ru.sweetbread.unn.Building
@ -56,7 +69,9 @@ import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
@Composable
fun Schedule() {
@ -132,26 +147,49 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
@Composable
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) {
fun getRatio(): Float {
val begin = LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
val end = LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
val now = LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
if (begin > now)
return -1f
if (now > end)
return 1f
return (now - begin) / (end - begin).toFloat()
}
val begin = unit.begin.format(DateTimeFormatter.ofPattern("HH:mm"))
val end = unit.end.format(DateTimeFormatter.ofPattern("HH:mm"))
var ratio by remember { mutableFloatStateOf(getRatio()) }
LaunchedEffect(Unit) {
while (true) {
val now = System.currentTimeMillis()
val calendar = Calendar.getInstance().apply { timeInMillis = now }
val seconds = calendar.get(Calendar.SECOND)
val millisUntilNextMinute = (60 - seconds) * 1000L - calendar.get(Calendar.MILLISECOND)
delay(millisUntilNextMinute)
ratio = getRatio()
}
}
val backgroundColor by animateColorAsState(
targetValue = if (rel == 1f)
MaterialTheme.colorScheme.surfaceContainer
else if (rel == -1f)
MaterialTheme.colorScheme.secondaryContainer
else
MaterialTheme.colorScheme.primaryContainer,
label = "backgroundTransition"
)
Row (
modifier
.fillMaxWidth()
.padding(4.dp)
.clip(RoundedCornerShape(8.dp))
.background(
if ((LocalDateTime.of(
unit.date,
unit.begin
) < LocalDateTime.now()) and (LocalDateTime.now() < LocalDateTime.of(
unit.date,
unit.end
))
)
MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.secondaryContainer
)
.background(backgroundColor)
.padding(8.dp)
){
Column (Modifier.weight(1f)) {
@ -240,8 +278,24 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
color = MaterialTheme.colorScheme.onBackground
)
Row (Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Row (Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(begin.toString(), fontWeight = FontWeight.Bold)
AnimatedVisibility (
(0f <= rel) and (rel < 1f),
modifier = Modifier
.weight(1f)
.padding(horizontal = 2.dp)
) {
DividerWithMarker(
positionPercentage = rel,
color = MaterialTheme.colorScheme.outline,
thickness = 3.dp,
markerSize = 8.dp,
markerColor = MaterialTheme.colorScheme.primary
)
}
Text(end.toString())
}
}
@ -257,6 +311,43 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
}
}
@Composable
fun DividerWithMarker(
modifier: Modifier = Modifier,
positionPercentage: Float,
color: Color = Color.Gray,
thickness: Dp = 1.dp,
markerSize: Dp = 8.dp,
markerColor: Color = Color.Red
) {
Canvas(modifier = modifier.height(thickness)) {
val dividerHeight = thickness.toPx()
val width = size.width
val markerX = width * positionPercentage
drawLine(
color = markerColor,
start = Offset(0f, dividerHeight / 2),
end = Offset(markerX, dividerHeight / 2),
strokeWidth = dividerHeight
)
drawLine(
color = color,
start = Offset(markerX, dividerHeight / 2),
end = Offset(width, dividerHeight / 2),
strokeWidth = dividerHeight / 2
)
drawCircle(
color = markerColor,
radius = markerSize.toPx() / 2,
center = Offset(markerX, dividerHeight / 2)
)
}
}
@Preview
@Composable
fun ScheduleItemPreview() {

View File

@ -1,3 +1,8 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.layout
import android.os.Bundle
@ -7,19 +12,20 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -74,21 +80,26 @@ class LoginActivity : ComponentActivity() {
SnackbarHost(hostState = snackbarHostState)
}
) { innerPadding ->
LoginPanel(Modifier.padding(innerPadding), { login, password ->
LoginData.login = login
LoginData.password = password
start<MainActivity>()
finish()
}, {
scope.launch {
snackbarHostState
.showSnackbar(
message = "Error",
duration = SnackbarDuration.Short
)
}
})
Box(Modifier.padding(innerPadding).fillMaxSize(), Alignment.Center) {
LoginPanel(
Modifier.imePadding(),
{ login, password ->
LoginData.login = login
LoginData.password = password
start<MainActivity>()
finish()
},
{
scope.launch {
snackbarHostState
.showSnackbar(
message = "Error",
duration = SnackbarDuration.Short
)
}
}
)
}
}
}
}
@ -107,48 +118,52 @@ fun LoginPanel(
var loading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize(), Alignment.BottomCenter) {
Column(
modifier
.padding(32.dp, 0.dp)
.clip(RoundedCornerShape(10.dp, 10.dp))
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(16.dp)
) {
TextField(
modifier = Modifier.padding(8.dp),
value = login,
onValueChange = { login = it },
singleLine = true,
label = { Text(stringResource(R.string.prompt_login)) }
)
Column(
modifier
.padding(32.dp, 0.dp)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
modifier = Modifier.padding(8.dp),
value = login,
onValueChange = { login = it },
singleLine = true,
label = { Text(stringResource(R.string.prompt_login))},
placeholder = { Text("s23380101") }
)
TextField(
modifier = Modifier.padding(8.dp),
value = password,
onValueChange = { password = it },
singleLine = true,
label = { Text(stringResource(R.string.prompt_password)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
OutlinedTextField(
modifier = Modifier.padding(8.dp),
value = password,
onValueChange = { password = it },
singleLine = true,
label = { Text(stringResource(R.string.prompt_password)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
Button(modifier = Modifier
.fillMaxWidth()
.padding(8.dp), onClick = {
Button(
modifier = Modifier.padding(8.dp),
enabled = login.trim().isNotEmpty() and password.trim().isNotEmpty() and !loading,
onClick = {
loading = true
scope.launch {
if (auth(login, password)) {
if (auth(login, password))
ok(login, password)
} else {
else
error()
}
loading = false
}
}) {
Text(stringResource(R.string.sign_in))
}
) {
if (loading)
CircularProgressIndicator()
else
Text(stringResource(R.string.sign_in))
}
}
}

View File

@ -1,3 +1,8 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.layout
import android.os.Bundle
@ -22,10 +27,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.room.Room
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry
@ -70,6 +75,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
UNNTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {

View File

@ -1,8 +1,14 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
// 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
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.1.20-2.0.0" apply false
}
buildscript {

View File

@ -1,27 +1,27 @@
[versions]
acraHttp = "5.11.3"
agp = "8.7.0"
calendar = "2.5.4"
agp = "8.7.3"
calendar = "2.6.2"
coilCompose = "2.7.0"
compose = "1.6.4" # Updating this will cause an error!
compose = "1.8.0"
coreSplashscreen = "1.0.1"
datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.1.2"
kotlin = "1.9.0"
coreKtx = "1.13.1"
datastorePreferences = "1.1.5"
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.5"
activityCompose = "1.9.2"
composeBom = "2024.03.00" # Updating this will cause an error!
lifecycle = "2.8.7"
activityCompose = "1.10.1"
composeBom = "2025.04.01"
appcompat = "1.7.0"
material = "1.12.0"
annotation = "1.8.2"
constraintlayout = "2.1.4"
activity = "1.9.2"
navigationCompose = "2.7.7" # Updating this will cause an error!
roomRuntime = "2.6.1"
annotation = "1.9.1"
constraintlayout = "2.2.1"
activity = "1.10.1"
navigationCompose = "2.8.9"
roomRuntime = "2.7.1"
secretsGradlePlugin = "2.0.1"
splitties = "3.0.0"
materialIconsCoreAndroid = "1.7.8"
@ -69,4 +69,5 @@ androidx-material-icons-core-android = { group = "androidx.compose.material", na
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }