Compare commits
10 Commits
315e7e4afe
...
95f84cafb7
Author | SHA1 | Date | |
---|---|---|---|
95f84cafb7 | |||
6260796084 | |||
269781586a | |||
b6eed728e3 | |||
3fcd4b013e | |||
57bca56d2b | |||
ad8ef6dca8 | |||
33959936d6 | |||
e9a200d543 | |||
282c93b5d4 |
@ -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,22 +34,29 @@ android {
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
// javaCompileOptions {
|
||||
// annotationProcessorOptions {
|
||||
// arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
resValue("string", "app_name", "@string/app_name_reg")
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix =
|
||||
"-debug+${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)}"
|
||||
resValue("string", "app_name", "app_name_dev")
|
||||
"-debug+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
|
||||
resValue("string", "app_name", "@string/app_name_dev")
|
||||
}
|
||||
create("beta") {
|
||||
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")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
@ -71,6 +84,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.material.icons.core.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
@ -102,14 +116,14 @@ dependencies {
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.glide)
|
||||
|
||||
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.kefirbb)
|
||||
|
||||
implementation(libs.acra.http)
|
||||
|
||||
implementation(libs.androidx.room.runtime)
|
||||
|
Binary file not shown.
Binary file not shown.
37
app/release/output-metadata.json
Normal file
37
app/release/output-metadata.json
Normal 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
|
||||
}
|
BIN
app/release/ru.sweetbread.unn-v1(1.0)-release.apk
Normal file
BIN
app/release/ru.sweetbread.unn-v1(1.0)-release.apk
Normal file
Binary file not shown.
@ -19,11 +19,14 @@ import ru.sweetbread.unn.db.loadSchedule
|
||||
import ru.sweetbread.unn.db.loadUserByBitrixId
|
||||
import ru.sweetbread.unn.ui.layout.LoginData
|
||||
import ru.sweetbread.unn.ui.layout.client
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
private lateinit var PHPSESSID: String
|
||||
private lateinit var CSRF: String
|
||||
lateinit var ME: User
|
||||
@ -31,8 +34,10 @@ lateinit var ME: User
|
||||
const val portalURL = "https://portal.unn.ru"
|
||||
const val ruzapiURL = "$portalURL/ruzapi"
|
||||
const val vuzapiURL = "$portalURL/bitrix/vuz/api"
|
||||
const val prtl2URL = "$portalURL/portal2/api"
|
||||
const val restURL = "$portalURL/rest"
|
||||
|
||||
|
||||
enum class Type(val s: String) {
|
||||
Student("student"),
|
||||
Group("group"),
|
||||
@ -109,7 +114,6 @@ class User(
|
||||
|
||||
class Post(
|
||||
val id: Int,
|
||||
val blogId: Int,
|
||||
val authorId: Int,
|
||||
val enableComments: Boolean,
|
||||
val numComments: Int,
|
||||
@ -123,6 +127,7 @@ class AvatarSet(
|
||||
val small: String
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Authorize user by [login] and [password]
|
||||
*
|
||||
@ -149,10 +154,8 @@ suspend fun auth(
|
||||
if (r.status.value == 302) {
|
||||
PHPSESSID =
|
||||
"""PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
getMyself(login)
|
||||
getCSRF()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -162,42 +165,44 @@ suspend fun auth(
|
||||
* Save info about current [User] in memory
|
||||
*/
|
||||
private suspend fun getMyself(login: String) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo") {
|
||||
parameter("uns", login.substring(1))
|
||||
}.bodyAsText())
|
||||
// WARNING: trailing / is important, 'cuz API devs are eating shit
|
||||
val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo/") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
parameter("uns", login.drop(1))
|
||||
}.bodyAsText())
|
||||
|
||||
val user = JSONObject(
|
||||
client.get("$vuzapiURL/user") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
}.bodyAsText()
|
||||
)
|
||||
val user = JSONObject(
|
||||
client.get("$vuzapiURL/user") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
}.bodyAsText()
|
||||
)
|
||||
|
||||
ME = User(
|
||||
unnId = studentinfo.getString("id").toInt(),
|
||||
bitrixId = user.getInt("bitrix_id"),
|
||||
userId = user.getInt("id"),
|
||||
type = when (studentinfo.getString("type")) {
|
||||
"lecturer" -> Type.Lecturer // ig,,,
|
||||
else -> Type.Student
|
||||
},
|
||||
email = user.getString("email"),
|
||||
nameRu = user.getString("fullname"),
|
||||
nameEn = user.getString("fullname_en"),
|
||||
isMale = user.getString("sex") == "M",
|
||||
birthday = LocalDate.parse(
|
||||
user.getString("birthdate"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
),
|
||||
avatar = user.getJSONObject("photo").let {
|
||||
AvatarSet(
|
||||
it.getString("orig"),
|
||||
it.getString("thumbnail"),
|
||||
it.getString("small"),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
Log.d("studentInfo", studentinfo.toString(2))
|
||||
|
||||
ME = User(
|
||||
unnId = studentinfo.getString("id").toInt(),
|
||||
bitrixId = user.getInt("bitrix_id"),
|
||||
userId = user.getInt("id"),
|
||||
type = when (studentinfo.getString("type")) {
|
||||
"lecturer" -> Type.Lecturer // ig,,,
|
||||
else -> Type.Student
|
||||
},
|
||||
email = user.getString("email"),
|
||||
nameRu = user.getString("fullname"),
|
||||
nameEn = user.getString("fullname_en"),
|
||||
isMale = user.getString("sex") == "M",
|
||||
birthday = Instant
|
||||
.parse(user.getString("birthdate"))
|
||||
.atZone(ZoneId.of("Europe/Moscow"))
|
||||
.toLocalDate(),
|
||||
avatar = user.getJSONObject("photo").let {
|
||||
AvatarSet(
|
||||
it.getString("orig"),
|
||||
it.getString("thumbnail"),
|
||||
it.getString("small"),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getScheduleDay(
|
||||
@ -209,9 +214,8 @@ suspend fun getScheduleDay(
|
||||
if ((type == ME.type) and (id == ME.unnId!!)) {
|
||||
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
|
||||
Log.d("Schedule", schedule.joinToString())
|
||||
if (schedule.size != 0) {
|
||||
if (schedule.isNotEmpty())
|
||||
return schedule
|
||||
}
|
||||
}
|
||||
|
||||
return getSchedule(type, id, date, date)
|
||||
@ -223,7 +227,7 @@ suspend fun getSchedule(
|
||||
start: LocalDate,
|
||||
finish: LocalDate
|
||||
): 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") {
|
||||
parameter("start", start.format(unnDatePattern))
|
||||
@ -310,28 +314,26 @@ suspend fun getCSRF() {
|
||||
}
|
||||
|
||||
suspend fun getBlogposts(): ArrayList<Post> {
|
||||
val r = client.get("$restURL/log.blogpost.get") {
|
||||
val r = client.get("$prtl2URL/news.php") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
parameter("sessid", CSRF)
|
||||
header("x-bitrix-sessid-token", CSRF)
|
||||
}
|
||||
val json = JSONObject(r.bodyAsText())
|
||||
val result = json.getJSONArray("result")
|
||||
val result = JSONArray(r.bodyAsText())
|
||||
|
||||
val out = arrayListOf<Post>()
|
||||
for (i in 0 until result.length()) {
|
||||
val el = result.getJSONObject(i)
|
||||
out.add(
|
||||
Post(
|
||||
id = el.getString("ID").toInt(),
|
||||
blogId = el.getString("BLOG_ID").toInt(),
|
||||
authorId = el.getString("AUTHOR_ID").toInt(),
|
||||
enableComments = el.getString("ENABLE_COMMENTS") == "Y",
|
||||
numComments = el.getString("NUM_COMMENTS").toInt(),
|
||||
id = el.getString("id").toInt(),
|
||||
authorId = el.getJSONObject("author").getInt("id").toInt(),
|
||||
enableComments = true, // FIXME: Delete the field or get correct value
|
||||
numComments = el.getString("commentsnum").toInt(),
|
||||
date = LocalDateTime.parse(
|
||||
el.getString("DATE_PUBLISH"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+03:00'")
|
||||
el.getString("time"),
|
||||
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"),
|
||||
nameEn = json.getString("fullname_en"),
|
||||
isMale = json.getString("sex") == "M",
|
||||
birthday = LocalDate.parse(
|
||||
json.getString("birthdate"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
),
|
||||
birthday = Instant
|
||||
.parse(json.getString("birthdate"))
|
||||
.atZone(ZoneId.of("Europe/Moscow"))
|
||||
.toLocalDate(),
|
||||
avatar = json.getJSONObject("photo").let {
|
||||
AvatarSet(
|
||||
it.getString("orig"),
|
||||
|
@ -11,15 +11,15 @@ class UNNApp : Application() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
httpSender {
|
||||
uri = BuildConfig.ACRA_URL
|
||||
basicAuthLogin = BuildConfig.ACRA_LOGIN
|
||||
basicAuthPassword = BuildConfig.ACRA_PASS
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
}
|
||||
// initAcra {
|
||||
// buildConfigClass = BuildConfig::class.java
|
||||
// reportFormat = StringFormat.JSON
|
||||
// httpSender {
|
||||
// uri = BuildConfig.ACRA_URL
|
||||
// basicAuthLogin = BuildConfig.ACRA_LOGIN
|
||||
// basicAuthPassword = BuildConfig.ACRA_PASS
|
||||
// httpMethod = HttpSender.Method.POST
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
@ -376,7 +376,7 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
|
||||
|
||||
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
|
||||
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(
|
||||
db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.mapNotNull { loadSchedule(it.oid) }
|
||||
|
@ -1,7 +1,16 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
*/
|
||||
|
||||
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
|
||||
@ -38,16 +47,14 @@ 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
|
||||
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
|
||||
@ -169,12 +176,11 @@ fun UserItem(modifier: Modifier = Modifier, user: User, info: String? = null) {
|
||||
@NonRestartableComposable
|
||||
fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = false) {
|
||||
var user: User? by remember { mutableStateOf(null) }
|
||||
val processor = remember { BBProcessorFactory.getInstance().create() }
|
||||
var html: String by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
LaunchedEffect(post) {
|
||||
html = toHtml(processor, post)
|
||||
html = post.content
|
||||
user = getUserByBitrixId(post.authorId)
|
||||
}
|
||||
|
||||
@ -194,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)
|
||||
@ -204,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
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@ -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
|
||||
@Composable
|
||||
@ -278,7 +252,6 @@ fun UserItemPreview() {
|
||||
fun PostItemPreview() {
|
||||
val post = Post(
|
||||
id = 154923,
|
||||
blogId = 121212,
|
||||
authorId = 165945,
|
||||
enableComments = true,
|
||||
numComments = 0,
|
||||
@ -295,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
|
||||
}
|
@ -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
|
||||
@ -51,11 +64,14 @@ import ru.sweetbread.unn.R
|
||||
import ru.sweetbread.unn.ScheduleUnit
|
||||
import ru.sweetbread.unn.getScheduleDay
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import splitties.resources.appStr
|
||||
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() {
|
||||
@ -101,12 +117,15 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
if (loadedDate == date) {
|
||||
Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}")
|
||||
LazyColumn (modifier) {
|
||||
items(lessons) { // TODO: Add empty list notification
|
||||
items(lessons) {
|
||||
ScheduleItem(unit = it, modifier = Modifier.clickable {
|
||||
expanded = if (it.oid == expanded) 0
|
||||
else it.oid
|
||||
}, expanded = expanded == it.oid)
|
||||
}
|
||||
|
||||
if (lessons.isEmpty())
|
||||
item { Text(appStr(R.string.noData)) }
|
||||
}
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
@ -128,26 +147,49 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
|
||||
@Composable
|
||||
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 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 (
|
||||
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)) {
|
||||
@ -236,8 +278,22 @@ 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)) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@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 ->
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -38,6 +43,7 @@ import ru.sweetbread.unn.db.AppDatabase
|
||||
import ru.sweetbread.unn.ui.composes.Blogposts
|
||||
import ru.sweetbread.unn.ui.composes.Schedule
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.toast.toast
|
||||
|
||||
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() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
db = Room.databaseBuilder(
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "database"
|
||||
).build()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
UNNTheme {
|
||||
|
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
BIN
app/src/main/res/drawable/unn_logo.png
Normal file
BIN
app/src/main/res/drawable/unn_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="app_name">ННГУ</string>
|
||||
<string name="prompt_login">Логин</string>
|
||||
<string name="sign_in">Войти</string>
|
||||
<string name="assistant">Ассистент</string>
|
||||
@ -11,5 +13,5 @@
|
||||
<string name="auditorium">Аудитория</string>
|
||||
<string name="building">Здание</string>
|
||||
<string name="floor">Этаж</string>
|
||||
<string name="app_name_beta">ННГУ Бета</string>
|
||||
<string name="noData">Нет данных</string>
|
||||
</resources>
|
@ -1,6 +1,8 @@
|
||||
<resources>
|
||||
<string name="app_name">UNN</string>
|
||||
<!-- <string name="title_activity_login">LoginActivity</string>-->
|
||||
<string name="app_name_reg">UNN</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_login">Login</string>
|
||||
<string name="prompt_password">Password</string>
|
||||
@ -13,7 +15,6 @@
|
||||
<string name="building">Building</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="app_name_dev" translatable="false">UNN Dev</string>
|
||||
<string name="app_name_beta">UNN Beta</string>
|
||||
<string name="noData">No Data</string>
|
||||
<!-- <string name="login_failed">"Login failed"</string>-->
|
||||
</resources>
|
36
app/src/main/res/xml/data_extraction_rules.xml
Normal file
36
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
@ -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 {
|
||||
|
@ -1,30 +1,31 @@
|
||||
[versions]
|
||||
acraHttp = "5.11.3"
|
||||
agp = "8.5.2"
|
||||
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"
|
||||
glide = "4.16.0"
|
||||
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"
|
||||
splittiesFunPackAndroidBaseWithViewsDsl = "3.0.0"
|
||||
kefirbb = "1.5"
|
||||
splitties = "3.0.0"
|
||||
materialIconsCoreAndroid = "1.7.8"
|
||||
|
||||
[libraries]
|
||||
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-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
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-cio = { module = "io.ktor:ktor-client-cio", 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-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" }
|
||||
splitties-funpack-android-base-with-views-dsl = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splittiesFunPackAndroidBaseWithViewsDsl" }
|
||||
kefirbb = { group = "org.kefirsf", name = "kefirbb", version.ref = "kefirbb" }
|
||||
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splitties" }
|
||||
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]
|
||||
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" }
|
||||
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Sat Mar 16 18:30:45 MSK 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
3
secrets.properties
Normal file
3
secrets.properties
Normal file
@ -0,0 +1,3 @@
|
||||
ACRA_URL=https://bugs.coders-squad.com/report
|
||||
ACRA_LOGIN=pMJkqPlNLX4kQ3kK
|
||||
ACRA_PASS=HYY72oV4ybmpCggC
|
Loading…
x
Reference in New Issue
Block a user