Compare commits

...

10 Commits

Author SHA1 Message Date
95f84cafb7
impr: show images in blogposts 2025-04-23 23:19:56 +03:00
6260796084
deps: update 2025-04-23 23:19:56 +03:00
269781586a
impr: add animation 2025-04-23 23:19:47 +03:00
b6eed728e3
impr: LoginActivity.kt
- Style changed
- Added loading indicator
- Added login/password validation
2025-04-23 23:19:34 +03:00
3fcd4b013e
feat: add marked divider 2025-04-23 23:19:34 +03:00
57bca56d2b
style: simplify db declaration 2025-04-23 23:19:06 +03:00
ad8ef6dca8
fix: builds variants names 2025-04-23 23:18:48 +03:00
33959936d6
fix: apply warning fixes 2025-04-23 23:18:48 +03:00
e9a200d543
impr: add empty list message 2025-04-23 23:18:48 +03:00
282c93b5d4
fix: apply API changes 2025-04-23 23:18:48 +03:00
22 changed files with 681 additions and 219 deletions

View File

@ -1,9 +1,15 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
plugins { plugins {
alias(libs.plugins.androidApplication) alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.kotlin.compose)
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
} }
@ -14,12 +20,12 @@ secrets {
android { android {
namespace = "ru.sweetbread.unn" namespace = "ru.sweetbread.unn"
compileSdk = 34 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "ru.sweetbread.unn" applicationId = "ru.sweetbread.unn"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)") setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
@ -28,22 +34,29 @@ android {
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
// javaCompileOptions {
// annotationProcessorOptions {
// arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
// }
// }
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
resValue("string", "app_name", "@string/app_name_reg")
} }
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
versionNameSuffix = versionNameSuffix =
"-debug+${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)}" "-debug+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
resValue("string", "app_name", "app_name_dev") resValue("string", "app_name", "@string/app_name_dev")
} }
create("beta") { create("beta") {
versionNameSuffix = versionNameSuffix =
"-beta+${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)}" "-beta+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
resValue("string", "app_name", "@string/app_name_beta") resValue("string", "app_name", "@string/app_name_beta")
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
} }
@ -71,6 +84,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.material.icons.core.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
@ -102,14 +116,14 @@ dependencies {
implementation(libs.ktor.client.logging) implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.android) implementation(libs.ktor.client.android)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.glide)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.splitties.funpack.android.base.with.views.dsl) implementation(libs.splitties.base)
implementation(libs.splitties.room)
implementation(libs.compose) implementation(libs.compose)
implementation(libs.kefirbb)
implementation(libs.acra.http) implementation(libs.acra.http)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)

View File

@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "ru.sweetbread.unn",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "ru.sweetbread.unn-v1(1.0)-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/ru.sweetbread.unn-v1(1.0)-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/ru.sweetbread.unn-v1(1.0)-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}

Binary file not shown.

View File

@ -19,11 +19,14 @@ import ru.sweetbread.unn.db.loadSchedule
import ru.sweetbread.unn.db.loadUserByBitrixId import ru.sweetbread.unn.db.loadUserByBitrixId
import ru.sweetbread.unn.ui.layout.LoginData import ru.sweetbread.unn.ui.layout.LoginData
import ru.sweetbread.unn.ui.layout.client import ru.sweetbread.unn.ui.layout.client
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
private lateinit var PHPSESSID: String private lateinit var PHPSESSID: String
private lateinit var CSRF: String private lateinit var CSRF: String
lateinit var ME: User lateinit var ME: User
@ -31,8 +34,10 @@ lateinit var ME: User
const val portalURL = "https://portal.unn.ru" const val portalURL = "https://portal.unn.ru"
const val ruzapiURL = "$portalURL/ruzapi" const val ruzapiURL = "$portalURL/ruzapi"
const val vuzapiURL = "$portalURL/bitrix/vuz/api" const val vuzapiURL = "$portalURL/bitrix/vuz/api"
const val prtl2URL = "$portalURL/portal2/api"
const val restURL = "$portalURL/rest" const val restURL = "$portalURL/rest"
enum class Type(val s: String) { enum class Type(val s: String) {
Student("student"), Student("student"),
Group("group"), Group("group"),
@ -109,7 +114,6 @@ class User(
class Post( class Post(
val id: Int, val id: Int,
val blogId: Int,
val authorId: Int, val authorId: Int,
val enableComments: Boolean, val enableComments: Boolean,
val numComments: Int, val numComments: Int,
@ -123,6 +127,7 @@ class AvatarSet(
val small: String val small: String
) )
/** /**
* Authorize user by [login] and [password] * Authorize user by [login] and [password]
* *
@ -149,10 +154,8 @@ suspend fun auth(
if (r.status.value == 302) { if (r.status.value == 302) {
PHPSESSID = PHPSESSID =
"""PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
GlobalScope.launch(Dispatchers.IO) {
getMyself(login) getMyself(login)
getCSRF() getCSRF()
}
return true return true
} }
return false return false
@ -162,9 +165,10 @@ suspend fun auth(
* Save info about current [User] in memory * Save info about current [User] in memory
*/ */
private suspend fun getMyself(login: String) { private suspend fun getMyself(login: String) {
GlobalScope.launch(Dispatchers.IO) { // WARNING: trailing / is important, 'cuz API devs are eating shit
val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo") { val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo/") {
parameter("uns", login.substring(1)) header("Cookie", "PHPSESSID=$PHPSESSID")
parameter("uns", login.drop(1))
}.bodyAsText()) }.bodyAsText())
val user = JSONObject( val user = JSONObject(
@ -173,6 +177,8 @@ private suspend fun getMyself(login: String) {
}.bodyAsText() }.bodyAsText()
) )
Log.d("studentInfo", studentinfo.toString(2))
ME = User( ME = User(
unnId = studentinfo.getString("id").toInt(), unnId = studentinfo.getString("id").toInt(),
bitrixId = user.getInt("bitrix_id"), bitrixId = user.getInt("bitrix_id"),
@ -185,10 +191,10 @@ private suspend fun getMyself(login: String) {
nameRu = user.getString("fullname"), nameRu = user.getString("fullname"),
nameEn = user.getString("fullname_en"), nameEn = user.getString("fullname_en"),
isMale = user.getString("sex") == "M", isMale = user.getString("sex") == "M",
birthday = LocalDate.parse( birthday = Instant
user.getString("birthdate"), .parse(user.getString("birthdate"))
DateTimeFormatter.ofPattern("yyyy-MM-dd") .atZone(ZoneId.of("Europe/Moscow"))
), .toLocalDate(),
avatar = user.getJSONObject("photo").let { avatar = user.getJSONObject("photo").let {
AvatarSet( AvatarSet(
it.getString("orig"), it.getString("orig"),
@ -197,7 +203,6 @@ private suspend fun getMyself(login: String) {
) )
} }
) )
}
} }
suspend fun getScheduleDay( suspend fun getScheduleDay(
@ -209,10 +214,9 @@ suspend fun getScheduleDay(
if ((type == ME.type) and (id == ME.unnId!!)) { if ((type == ME.type) and (id == ME.unnId!!)) {
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) } val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
Log.d("Schedule", schedule.joinToString()) Log.d("Schedule", schedule.joinToString())
if (schedule.size != 0) { if (schedule.isNotEmpty())
return schedule return schedule
} }
}
return getSchedule(type, id, date, date) return getSchedule(type, id, date, date)
} }
@ -223,7 +227,7 @@ suspend fun getSchedule(
start: LocalDate, start: LocalDate,
finish: LocalDate finish: LocalDate
): ArrayList<ScheduleUnit> { ): ArrayList<ScheduleUnit> {
val unnDatePattern = DateTimeFormatter.ofPattern("yyyy.MM.dd") val unnDatePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val r = client.get("$ruzapiURL/schedule/${type.s}/$id") { val r = client.get("$ruzapiURL/schedule/${type.s}/$id") {
parameter("start", start.format(unnDatePattern)) parameter("start", start.format(unnDatePattern))
@ -310,28 +314,26 @@ suspend fun getCSRF() {
} }
suspend fun getBlogposts(): ArrayList<Post> { suspend fun getBlogposts(): ArrayList<Post> {
val r = client.get("$restURL/log.blogpost.get") { val r = client.get("$prtl2URL/news.php") {
header("Cookie", "PHPSESSID=$PHPSESSID") header("Cookie", "PHPSESSID=$PHPSESSID")
parameter("sessid", CSRF) header("x-bitrix-sessid-token", CSRF)
} }
val json = JSONObject(r.bodyAsText()) val result = JSONArray(r.bodyAsText())
val result = json.getJSONArray("result")
val out = arrayListOf<Post>() val out = arrayListOf<Post>()
for (i in 0 until result.length()) { for (i in 0 until result.length()) {
val el = result.getJSONObject(i) val el = result.getJSONObject(i)
out.add( out.add(
Post( Post(
id = el.getString("ID").toInt(), id = el.getString("id").toInt(),
blogId = el.getString("BLOG_ID").toInt(), authorId = el.getJSONObject("author").getInt("id").toInt(),
authorId = el.getString("AUTHOR_ID").toInt(), enableComments = true, // FIXME: Delete the field or get correct value
enableComments = el.getString("ENABLE_COMMENTS") == "Y", numComments = el.getString("commentsnum").toInt(),
numComments = el.getString("NUM_COMMENTS").toInt(),
date = LocalDateTime.parse( date = LocalDateTime.parse(
el.getString("DATE_PUBLISH"), el.getString("time"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+03:00'") DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
), ),
content = el.getString("DETAIL_TEXT") content = el.getString("fulltext")
) )
) )
} }
@ -375,10 +377,10 @@ suspend fun getUser(id: Int): User {
nameRu = json.getString("fullname"), nameRu = json.getString("fullname"),
nameEn = json.getString("fullname_en"), nameEn = json.getString("fullname_en"),
isMale = json.getString("sex") == "M", isMale = json.getString("sex") == "M",
birthday = LocalDate.parse( birthday = Instant
json.getString("birthdate"), .parse(json.getString("birthdate"))
DateTimeFormatter.ofPattern("yyyy-MM-dd") .atZone(ZoneId.of("Europe/Moscow"))
), .toLocalDate(),
avatar = json.getJSONObject("photo").let { avatar = json.getJSONObject("photo").let {
AvatarSet( AvatarSet(
it.getString("orig"), it.getString("orig"),

View File

@ -11,15 +11,15 @@ class UNNApp : Application() {
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) super.attachBaseContext(base)
initAcra { // initAcra {
buildConfigClass = BuildConfig::class.java // buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON // reportFormat = StringFormat.JSON
httpSender { // httpSender {
uri = BuildConfig.ACRA_URL // uri = BuildConfig.ACRA_URL
basicAuthLogin = BuildConfig.ACRA_LOGIN // basicAuthLogin = BuildConfig.ACRA_LOGIN
basicAuthPassword = BuildConfig.ACRA_PASS // basicAuthPassword = BuildConfig.ACRA_PASS
httpMethod = HttpSender.Method.POST // httpMethod = HttpSender.Method.POST
} // }
} // }
} }
} }

View File

@ -376,7 +376,7 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> { fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE)) db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") } .map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
return ArrayList( return ArrayList(
db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE)) db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { loadSchedule(it.oid) } .mapNotNull { loadSchedule(it.oid) }

View File

@ -1,7 +1,16 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.composes 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.text.util.Linkify
import android.util.Log import android.util.Log
import android.widget.TextView
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
@ -38,16 +47,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.android.material.textview.MaterialTextView import coil.request.ImageRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import org.kefirsf.bb.BBProcessorFactory
import org.kefirsf.bb.TextProcessor
import ru.sweetbread.unn.AvatarSet import ru.sweetbread.unn.AvatarSet
import ru.sweetbread.unn.Post import ru.sweetbread.unn.Post
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
@ -169,12 +176,11 @@ fun UserItem(modifier: Modifier = Modifier, user: User, info: String? = null) {
@NonRestartableComposable @NonRestartableComposable
fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = false) { fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = false) {
var user: User? by remember { mutableStateOf(null) } var user: User? by remember { mutableStateOf(null) }
val processor = remember { BBProcessorFactory.getInstance().create() }
var html: String by remember { mutableStateOf("") } var html: String by remember { mutableStateOf("") }
LaunchedEffect(post) { LaunchedEffect(post) {
html = toHtml(processor, post) html = post.content
user = getUserByBitrixId(post.authorId) user = getUserByBitrixId(post.authorId)
} }
@ -194,8 +200,10 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
val linkColor = MaterialTheme.colorScheme.primary.toArgb() val linkColor = MaterialTheme.colorScheme.primary.toArgb()
AndroidView( AndroidView(
modifier = Modifier,
factory = { factory = {
MaterialTextView(it).apply { TextView(it).apply {
movementMethod = LinkMovementMethod.getInstance()
autoLinkMask = Linkify.WEB_URLS autoLinkMask = Linkify.WEB_URLS
linksClickable = true linksClickable = true
setTextColor(textColor) setTextColor(textColor)
@ -204,7 +212,12 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
}, },
update = { update = {
it.maxLines = if (extended) Int.MAX_VALUE else 5 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
)
} }
) )
@ -218,45 +231,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 @Preview
@Composable @Composable
@ -278,7 +252,6 @@ fun UserItemPreview() {
fun PostItemPreview() { fun PostItemPreview() {
val post = Post( val post = Post(
id = 154923, id = 154923,
blogId = 121212,
authorId = 165945, authorId = 165945,
enableComments = true, enableComments = true,
numComments = 0, numComments = 0,
@ -296,3 +269,76 @@ 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 package ru.sweetbread.unn.ui.composes
import android.util.Log import android.util.Log
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
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
@ -10,6 +17,7 @@ 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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -23,6 +31,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -31,15 +40,19 @@ import androidx.compose.runtime.setValue
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.geometry.Offset
import androidx.compose.ui.graphics.Color
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
import androidx.compose.ui.tooling.preview.Preview 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.zIndex import androidx.compose.ui.zIndex
import com.kizitonwose.calendar.compose.WeekCalendar import com.kizitonwose.calendar.compose.WeekCalendar
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.sweetbread.unn.Auditorium import ru.sweetbread.unn.Auditorium
import ru.sweetbread.unn.Building import ru.sweetbread.unn.Building
@ -51,11 +64,14 @@ import ru.sweetbread.unn.R
import ru.sweetbread.unn.ScheduleUnit import ru.sweetbread.unn.ScheduleUnit
import ru.sweetbread.unn.getScheduleDay import ru.sweetbread.unn.getScheduleDay
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr
import java.time.DayOfWeek 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
import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Calendar
@Composable @Composable
fun Schedule() { fun Schedule() {
@ -101,12 +117,15 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
if (loadedDate == date) { if (loadedDate == date) {
Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}") Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}")
LazyColumn (modifier) { LazyColumn (modifier) {
items(lessons) { // TODO: Add empty list notification items(lessons) {
ScheduleItem(unit = it, modifier = Modifier.clickable { ScheduleItem(unit = it, modifier = Modifier.clickable {
expanded = if (it.oid == expanded) 0 expanded = if (it.oid == expanded) 0
else it.oid else it.oid
}, expanded = expanded == it.oid) }, expanded = expanded == it.oid)
} }
if (lessons.isEmpty())
item { Text(appStr(R.string.noData)) }
} }
} else { } else {
LinearProgressIndicator( LinearProgressIndicator(
@ -128,26 +147,49 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
@Composable @Composable
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) { fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) {
fun getRel(): 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 begin = unit.begin.format(DateTimeFormatter.ofPattern("HH:mm"))
val end = unit.end.format(DateTimeFormatter.ofPattern("HH:mm")) val end = unit.end.format(DateTimeFormatter.ofPattern("HH:mm"))
var rel by remember { mutableFloatStateOf(getRel()) }
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()
}
}
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 ( Row (
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(4.dp) .padding(4.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background( .background(backgroundColor)
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
)
.padding(8.dp) .padding(8.dp)
){ ){
Column (Modifier.weight(1f)) { Column (Modifier.weight(1f)) {
@ -236,8 +278,22 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
color = MaterialTheme.colorScheme.onBackground 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) Text(begin.toString(), fontWeight = FontWeight.Bold)
AnimatedVisibility ((0f < rel) and (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()) Text(end.toString())
} }
} }
@ -253,6 +309,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 @Preview
@Composable @Composable
fun ScheduleItemPreview() { fun ScheduleItemPreview() {

View File

@ -1,3 +1,8 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.layout package ru.sweetbread.unn.ui.layout
import android.os.Bundle import android.os.Bundle
@ -7,19 +12,20 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize 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.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -74,21 +80,26 @@ class LoginActivity : ComponentActivity() {
SnackbarHost(hostState = snackbarHostState) SnackbarHost(hostState = snackbarHostState)
} }
) { innerPadding -> ) { innerPadding ->
LoginPanel(Modifier.padding(innerPadding), { login, password -> Box(Modifier.padding(innerPadding).fillMaxSize(), Alignment.Center) {
LoginPanel(
Modifier.imePadding(),
{ login, password ->
LoginData.login = login LoginData.login = login
LoginData.password = password LoginData.password = password
start<MainActivity>() start<MainActivity>()
finish() finish()
}, { },
{
scope.launch { scope.launch {
snackbarHostState snackbarHostState
.showSnackbar( .showSnackbar(
message = "Error", message = "Error",
duration = SnackbarDuration.Short duration = SnackbarDuration.Short
) )
} }
}) }
)
}
} }
} }
} }
@ -107,23 +118,24 @@ fun LoginPanel(
var loading by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize(), Alignment.BottomCenter) {
Column( Column(
modifier modifier
.padding(32.dp, 0.dp) .padding(32.dp, 0.dp)
.clip(RoundedCornerShape(10.dp, 10.dp)) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.primaryContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
.padding(16.dp) .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
TextField( OutlinedTextField(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
value = login, value = login,
onValueChange = { login = it }, onValueChange = { login = it },
singleLine = true, 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), modifier = Modifier.padding(8.dp),
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
@ -133,22 +145,25 @@ fun LoginPanel(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
) )
Button(modifier = Modifier Button(
.fillMaxWidth() modifier = Modifier.padding(8.dp),
.padding(8.dp), onClick = { enabled = login.trim().isNotEmpty() and password.trim().isNotEmpty() and !loading,
onClick = {
loading = true loading = true
scope.launch { scope.launch {
if (auth(login, password)) { if (auth(login, password))
ok(login, password) ok(login, password)
} else { else
error() error()
}
loading = false 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 package ru.sweetbread.unn.ui.layout
import android.os.Bundle import android.os.Bundle
@ -22,10 +27,10 @@ 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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.room.Room
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpRequestRetry
@ -38,6 +43,7 @@ import ru.sweetbread.unn.db.AppDatabase
import ru.sweetbread.unn.ui.composes.Blogposts import ru.sweetbread.unn.ui.composes.Blogposts
import ru.sweetbread.unn.ui.composes.Schedule import ru.sweetbread.unn.ui.composes.Schedule
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.arch.room.roomDb
import splitties.toast.toast import splitties.toast.toast
val client = HttpClient(Android) { val client = HttpClient(Android) {
@ -62,17 +68,14 @@ val client = HttpClient(Android) {
} }
} }
lateinit var db: AppDatabase val db = roomDb<AppDatabase>(name = "database")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
db = Room.databaseBuilder( WindowCompat.setDecorFitsSystemWindows(window, false)
applicationContext,
AppDatabase::class.java, "database"
).build()
setContent { setContent {
UNNTheme { UNNTheme {

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name_reg">ННГУ</string>
<string name="app_name_dev">ННГУ Альфа</string>
<string name="app_name_beta">ННГУ Бета</string>
<string name="prompt_password">Пароль</string> <string name="prompt_password">Пароль</string>
<string name="app_name">ННГУ</string>
<string name="prompt_login">Логин</string> <string name="prompt_login">Логин</string>
<string name="sign_in">Войти</string> <string name="sign_in">Войти</string>
<string name="assistant">Ассистент</string> <string name="assistant">Ассистент</string>
@ -11,5 +13,5 @@
<string name="auditorium">Аудитория</string> <string name="auditorium">Аудитория</string>
<string name="building">Здание</string> <string name="building">Здание</string>
<string name="floor">Этаж</string> <string name="floor">Этаж</string>
<string name="app_name_beta">ННГУ Бета</string> <string name="noData">Нет данных</string>
</resources> </resources>

View File

@ -1,6 +1,8 @@
<resources> <resources>
<string name="app_name">UNN</string> <string name="app_name_reg">UNN</string>
<!-- <string name="title_activity_login">LoginActivity</string>--> <string name="app_name_dev">UNN Dev</string>
<string name="app_name_beta">UNN Beta</string>
<!-- <string name="title_activity_login">LoginActivity</string>-->
<string name="prompt_email" translatable="false">Email</string> <string name="prompt_email" translatable="false">Email</string>
<string name="prompt_login">Login</string> <string name="prompt_login">Login</string>
<string name="prompt_password">Password</string> <string name="prompt_password">Password</string>
@ -13,7 +15,6 @@
<string name="building">Building</string> <string name="building">Building</string>
<string name="floor">Floor</string> <string name="floor">Floor</string>
<string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string> <string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string>
<string name="app_name_dev" translatable="false">UNN Dev</string> <string name="noData">No Data</string>
<string name="app_name_beta">UNN Beta</string>
<!-- <string name="login_failed">"Login failed"</string>--> <!-- <string name="login_failed">"Login failed"</string>-->
</resources> </resources>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!--
TODO: Use <include> and <exclude> to control what is backed up.
The domain can be file, database, sharedpref, external or root.
Examples:
<include domain="file" path="file_to_include"/>
<exclude domain="file" path="file_to_exclude"/>
<include domain="file" path="include_folder"/>
<exclude domain="file" path="include_folder/file_to_exclude"/>
<exclude domain="file" path="exclude_folder"/>
<include domain="file" path="exclude_folder/file_to_include"/>
<include domain="sharedpref" path="include_shared_pref1.xml"/>
<include domain="database" path="db_name/file_to_include"/>
<exclude domain="database" path="db_name/include_folder/file_to_exclude"/>
<include domain="external" path="file_to_include"/>
<exclude domain="external" path="file_to_exclude"/>
<include domain="root" path="file_to_include"/>
<exclude domain="root" path="file_to_exclude"/>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

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. // 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
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 { buildscript {

View File

@ -1,30 +1,31 @@
[versions] [versions]
acraHttp = "5.11.3" acraHttp = "5.11.3"
agp = "8.5.2" agp = "8.7.3"
calendar = "2.5.4" calendar = "2.6.2"
coilCompose = "2.7.0" coilCompose = "2.7.0"
compose = "1.6.4" # Updating this will cause an error! compose = "1.8.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
datastorePreferences = "1.1.1" datastorePreferences = "1.1.5"
desugar_jdk_libs = "2.1.2" desugar_jdk_libs = "2.1.5"
kotlin = "1.9.0" glide = "4.16.0"
coreKtx = "1.13.1" kotlin = "2.1.20"
coreKtx = "1.16.0"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
ktor = "2.3.12" ktor = "2.3.12"
lifecycle = "2.8.5" lifecycle = "2.8.7"
activityCompose = "1.9.2" activityCompose = "1.10.1"
composeBom = "2024.03.00" # Updating this will cause an error! composeBom = "2025.04.01"
appcompat = "1.7.0" appcompat = "1.7.0"
material = "1.12.0" material = "1.12.0"
annotation = "1.8.2" annotation = "1.9.1"
constraintlayout = "2.1.4" constraintlayout = "2.2.1"
activity = "1.9.2" activity = "1.10.1"
navigationCompose = "2.7.7" # Updating this will cause an error! navigationCompose = "2.8.9"
roomRuntime = "2.6.1" roomRuntime = "2.7.1"
secretsGradlePlugin = "2.0.1" secretsGradlePlugin = "2.0.1"
splittiesFunPackAndroidBaseWithViewsDsl = "3.0.0" splitties = "3.0.0"
kefirbb = "1.5" materialIconsCoreAndroid = "1.7.8"
[libraries] [libraries]
acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" } acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" }
@ -51,6 +52,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
@ -62,10 +64,12 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
splitties-funpack-android-base-with-views-dsl = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splittiesFunPackAndroidBaseWithViewsDsl" } splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splitties" }
kefirbb = { group = "org.kefirsf", name = "kefirbb", version.ref = "kefirbb" } splitties-room = { module = "com.louiscad.splitties:splitties-arch-room", version.ref = "splitties" }
androidx-material-icons-core-android = { group = "androidx.compose.material", name = "material-icons-core-android", version.ref = "materialIconsCoreAndroid" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@ -1,6 +1,6 @@
#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.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

3
secrets.properties Normal file
View File

@ -0,0 +1,3 @@
ACRA_URL=https://bugs.coders-squad.com/report
ACRA_LOGIN=pMJkqPlNLX4kQ3kK
ACRA_PASS=HYY72oV4ybmpCggC