Compare commits

..
3 Commits
Author SHA1 Message Date
Sweetbread 315e7e4afe fixup! fix: apply API changes 2025-04-23 17:30:34 +03:00
Sweetbread 55ff6e6cd1 impr: LoginActivity.kt
- Style changed
- Added loading indicator
- Added login/password validation
2025-04-23 16:38:29 +03:00
Sweetbread 5e15a75664 feat: add marked divider 2025-04-23 14:52:06 +03:00
3 changed files with 130 additions and 72 deletions

No files matched your search

@@ -1,3 +1,8 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.composes
import android.text.util.Linkify
@@ -47,7 +52,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.kefirsf.bb.BBProcessorFactory
import org.kefirsf.bb.TextProcessor
import ru.sweetbread.unn.AvatarSet
import ru.sweetbread.unn.Post
import ru.sweetbread.unn.R
@@ -174,7 +178,7 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
LaunchedEffect(post) {
html = toHtml(processor, post)
html = post.content
user = getUserByBitrixId(post.authorId)
}
@@ -218,45 +222,6 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
}
}
private fun toHtml(
processor: TextProcessor,
post: Post
): String {
Log.d("toHTML | original", post.content)
val result =
post.content.replace("""\[URL=(.+?)](.+?)\[/URL]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<a href=\"${it.groups[1]?.value}\">${it.groups[2]?.value}</a>"
}.replace("""\[FONT=(.+?)](.*?)\[/FONT]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<span style=\"font-family: ${it.groups[1]?.value}\">${it.groups[2]?.value}</span>"
}.replace("""\[SIZE=(.+?)](.*?)\[/SIZE]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<span style=\"font-size: ${it.groups[1]?.value}\">${it.groups[2]?.value}</span>"
}.replace(
"""\[CENTER]\[JUSTIFY]\[CENTER](.*?)\[/CENTER]\[/JUSTIFY]\[/CENTER]""".toRegex(
RegexOption.DOT_MATCHES_ALL
)
) {
"<span style=\"text-align: center;\">${it.groups[1]?.value}</span><br>"
}.replace("""\[CENTER](.*?)\[/CENTER]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<span style=\"text-align: center;\">${it.groups[1]?.value}</span><br>"
}.replace("""\[JUSTIFY](.*?)\[/JUSTIFY]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<span style=\"text-align: justify;\">${it.groups[1]?.value}</span><br>"
}.replace("""\[B](.*?)\[/B]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<b>${it.groups[1]?.value}</b>"
}.replace("""\[U](.*?)\[/U]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<u>${it.groups[1]?.value}</u>"
}.replace("""\[P](.*?)\[/P]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<p>${it.groups[1]?.value}</p>"
/*}.replace("""\[DISK FILE ID=n(\d+)]""".toRegex(RegexOption.DOT_MATCHES_ALL)) {
"<img src=\"$portalURL/bitrix/tools/disk/uf.php?attachedId=${it.groups[1]?.value}&action=download&ncc=1\" />"*/
}.replace("""\[IMG .+].+?\[/IMG]""".toRegex(RegexOption.DOT_MATCHES_ALL), "")
/*.replace("\n", "\n<br>")*/
Log.d("toHTML | result", result)
return result
}
@Preview
@Composable
@@ -2,6 +2,7 @@ package ru.sweetbread.unn.ui.composes
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -10,6 +11,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 +25,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 +34,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 +63,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,8 +141,37 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
@Composable
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) {
fun getRel(begin: Long, end: Long, now: Long): Float {
if ((begin > now) or (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 rel by remember {
mutableFloatStateOf(getRel(
LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
))
}
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)
rel = getRel(
LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
)
}
}
Row (
modifier
@@ -141,16 +179,9 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
.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
))
)
if (rel != -1f)
MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surfaceContainer
)
.padding(8.dp)
){
@@ -240,8 +271,18 @@ 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)
if (rel != -1f)
DividerWithMarker(
Modifier.weight(1f).padding(horizontal = 2.dp),
positionPercentage = rel,
color = MaterialTheme.colorScheme.outline,
thickness = 3.dp,
markerSize = 8.dp,
markerColor = MaterialTheme.colorScheme.primary)
Text(end.toString())
}
}
@@ -257,6 +298,43 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
}
}
@Composable
fun DividerWithMarker(
modifier: Modifier = Modifier,
positionPercentage: Float, // от 0f до 1f (например, 0.5f = 50%)
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() {
@@ -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 ->
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,23 +118,24 @@ 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)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
OutlinedTextField(
modifier = Modifier.padding(8.dp),
value = login,
onValueChange = { login = it },
singleLine = true,
label = { Text(stringResource(R.string.prompt_login)) }
label = { Text(stringResource(R.string.prompt_login))},
placeholder = { Text("s23380101") }
)
TextField(
OutlinedTextField(
modifier = Modifier.padding(8.dp),
value = password,
onValueChange = { password = it },
@@ -133,22 +145,25 @@ fun LoginPanel(
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))
}
}
}