Compare commits

..

8 Commits

Author SHA1 Message Date
Sweetbread 315e7e4afe fixup! fix: apply API changes 2025-04-23 17:30:34 +03:00
Sweetbread 55ff6e6cd1 impr: LoginActivity.kt
- Style changed
- Added loading indicator
- Added login/password validation
2025-04-23 16:38:29 +03:00
Sweetbread 5e15a75664 feat: add marked divider 2025-04-23 14:52:06 +03:00
Sweetbread d7a2a26097 style: simplify db declaration 2025-04-23 00:36:20 +03:00
Sweetbread f1b330c1ce fix: builds variants names 2025-04-22 23:24:44 +03:00
Sweetbread 1ede1a4c2d fix: apply warning fixes 2025-04-22 20:28:36 +03:00
Sweetbread 08949c5f0d impr: add empty list message 2025-04-22 18:44:04 +03:00
Sweetbread be230e8a11 fix: apply API changes 2025-04-22 18:27:44 +03:00
48 changed files with 515 additions and 1481 deletions
-1
View File
@@ -8,4 +8,3 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
secrets.properties
-15
View File
@@ -1,15 +0,0 @@
<UNN HUB - unofficial app providing schedule, news and document access for students>
Copyright (C) 2025 Gleb Zaharov
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-70
View File
@@ -1,70 +0,0 @@
# NOTICE
This project incorporates components from the following third-party libraries:
## AndroidX Libraries
- **androidx.*** (Core, Room, Navigation, Compose, etc.)
Copyright The Android Open Source Project
Licensed under Apache License 2.0
[https://developer.android.com](https://developer.android.com)
## Coil
- **io.coil-kt:coil-compose**
Copyright Coil Contributors
Licensed under Apache License 2.0
[https://github.com/coil-kt/coil](https://github.com/coil-kt/coil)
## Compose Calendar
- **com.kizitonwose.calendar:compose**
Copyright Kizito Nwose
Licensed under MIT License
[https://github.com/kizitonwose/Calendar](https://github.com/kizitonwose/Calendar)
## Desugar JDK Libs
- **com.android.tools:desugar_jdk_libs**
Copyright Google LLC
Licensed under Apache License 2.0
## Ktor
- **io.ktor:ktor-***
Copyright JetBrains s.r.o.
Licensed under Apache License 2.0
[https://ktor.io](https://ktor.io)
## Material Components
- **com.google.android.material:material**
Copyright Google LLC
Licensed under Apache License 2.0
## Splitties
- **com.louiscad.splitties:***
Copyright Louis CAD
Licensed under Apache License 2.0
[https://github.com/LouisCAD/Splitties](https://github.com/LouisCAD/Splitties)
## Secrets Gradle Plugin
- **com.google.android.libraries.mapsplatform.secrets-gradle-plugin**
Copyright Google LLC
Licensed under Apache License 2.0
## Sentry Android
- **io.sentry:sentry-android**
Copyright Sentry and contributors
Licensed under MIT License
[https://github.com/getsentry/sentry-java](https://github.com/getsentry/sentry-java)
## Build Tools
- **Gradle Plugins** (Android, Kotlin)
Copyright Google LLC / JetBrains s.r.o.
Licensed under Apache License 2.0
---
### License Notices
This product includes software developed by:
- The Android Open Source Project (AOSP)
- JetBrains s.r.o.
- Google LLC
- And other contributors listed above
Full license texts are available in the [LICENSE](LICENSE) file and respective library repositories.
+11 -38
View File
@@ -1,17 +1,11 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Properties
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")
id("io.sentry.android.gradle") version "5.5.0"
} }
secrets { secrets {
@@ -20,34 +14,20 @@ secrets {
android { android {
namespace = "ru.sweetbread.unn" namespace = "ru.sweetbread.unn"
compileSdk = 36 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "ru.sweetbread.unn" applicationId = "ru.sweetbread.unn"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
val secretProperties = Properties().apply {
val secretFile = rootProject.file("secrets.properties")
if (secretFile.exists())
secretFile.inputStream().use { load(it) }
else
println("Warning: secrets.properties not found!")
}
manifestPlaceholders["sentry_url"] = secretProperties.getProperty("SENTRY_URL")!!
// javaCompileOptions {
// annotationProcessorOptions {
// arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
// }
// }
} }
buildTypes { buildTypes {
@@ -55,27 +35,27 @@ android {
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") resValue("string", "app_name", "@string/app_name_reg")
manifestPlaceholders["sentry_env"] = "production"
} }
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
versionNameSuffix = versionNameSuffix =
"-debug+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}" "-debug+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
resValue("string", "app_name", "@string/app_name_dev") resValue("string", "app_name", "@string/app_name_dev")
manifestPlaceholders["sentry_env"] = "debug"
} }
create("beta") { create("beta") {
versionNameSuffix = versionNameSuffix =
"-beta+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}" "-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")
manifestPlaceholders["sentry_env"] = "beta"
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = true viewBinding = true
@@ -91,18 +71,7 @@ android {
} }
} }
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
base {
archivesName = "ru.sweetbread.unn-v${android.defaultConfig.versionCode}(${android.defaultConfig.versionName})"
}
dependencies { dependencies {
implementation(libs.androidx.material.icons.core.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
@@ -139,7 +108,11 @@ dependencies {
implementation(libs.splitties.base) implementation(libs.splitties.base)
implementation(libs.splitties.room) implementation(libs.splitties.room)
implementation(libs.sentry) implementation(libs.compose)
implementation(libs.kefirbb)
implementation(libs.acra.http)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
-37
View File
@@ -1,37 +0,0 @@
{
"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.
@@ -1,13 +1,13 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn package ru.sweetbread.unn
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.* import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
-31
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
@@ -15,35 +13,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.UNN" android:theme="@style/Theme.UNN"
tools:targetApi="31"> tools:targetApi="31">
<meta-data
android:name="io.sentry.dsn"
android:value="${sentry_url}" />
<meta-data
android:name="io.sentry.environment"
android:value="${sentry_env}" />
<meta-data
android:name="io.sentry.send-default-pii"
android:value="true" />
<meta-data
android:name="io.sentry.traces.sample-rate"
android:value="1.0" />
<meta-data
android:name="io.sentry.traces.user-interaction.enable"
android:value="true" />
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" />
<meta-data
android:name="io.sentry.attach-view-hierarchy"
android:value="true" />
<meta-data
android:name="io.sentry.session-replay.on-error-sample-rate"
android:value="1.0" />
<meta-data
android:name="io.sentry.session-replay.session-sample-rate"
android:value="0.1" />
<activity <activity
android:name=".ui.layout.MainActivity" android:name=".ui.layout.MainActivity"
android:exported="true" android:exported="true"
@@ -1,16 +1,6 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). package ru.sweetbread.unn
package ru.sweetbread.unn.api
import android.util.Log import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.forms.submitForm import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.header import io.ktor.client.request.header
@@ -18,11 +8,17 @@ import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.parameters import io.ktor.http.parameters
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import ru.sweetbread.unn.db.cacheSchedule
import ru.sweetbread.unn.db.cacheUser import ru.sweetbread.unn.db.cacheUser
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.client
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -31,6 +27,10 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
private lateinit var PHPSESSID: String
private lateinit var CSRF: String
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"
@@ -38,29 +38,110 @@ const val prtl2URL = "$portalURL/portal2/api"
const val restURL = "$portalURL/rest" const val restURL = "$portalURL/rest"
val client = HttpClient(Android) { enum class Type(val s: String) {
install(HttpCache) Student("student"),
install(Logging) { Group("group"),
logger = object : Logger { Lecturer("lecturer"),
override fun log(message: String) { Auditorium("auditorium"),
Log.i("Ktor", message) Employee("employee")
}
}
level = LogLevel.ALL
}
install(HttpTimeout) {
connectTimeoutMillis = 5000
}
install(HttpRequestRetry) {
retryOnException(maxRetries = 3, retryOnTimeout = true)
exponentialDelay()
modifyRequest { request ->
request.headers.append("x-retry-count", retryCount.toString())
}
}
} }
suspend fun getToken(login: String, password: String): String? { enum class LecturerRank(val id: Int) {
Assistant(R.string.assistant),
Lecturer(R.string.lecturer),
SLecturer(R.string.slecturer),
AProfessor(R.string.aprofessor)
}
class ScheduleUnit(
val oid: Int,
val auditorium: Auditorium,
val date: LocalDate,
val discipline: Discipline,
val kindOfWork: KindOfWork,
val lecturers: ArrayList<Lecturer>,
val stream: String,
val begin: LocalTime,
val end: LocalTime
)
class Auditorium(
val name: String,
val oid: Int,
val floor: Int,
val building: Building
)
class Building(
val name: String,
val gid: Int,
val oid: Int
)
class Discipline(
val name: String,
val oid: Int,
val type: Int
)
class KindOfWork(
val name: String,
val oid: Int,
val uid: String,
val complexity: Int
)
class Lecturer(
val name: String,
val rank: LecturerRank,
val email: String,
val unnId: Int,
val uid: String
)
class User(
val unnId: Int?,
val bitrixId: Int,
val userId: Int,
val type: Type,
val email: String,
val nameRu: String,
val nameEn: String,
val isMale: Boolean,
val birthday: LocalDate,
val avatar: AvatarSet
)
class Post(
val id: Int,
val authorId: Int,
val enableComments: Boolean,
val numComments: Int,
val date: LocalDateTime,
val content: String
)
class AvatarSet(
val original: String,
val thumbnail: String,
val small: String
)
/**
* Authorize user by [login] and [password]
*
* Also defines local vars [PHPSESSID] and [ME]
*/
suspend fun auth(
login: String = LoginData.login,
password: String = LoginData.password,
forced: Boolean = false
): Boolean {
if (!forced) {
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
return true
}
val r = client.submitForm("$portalURL/auth/?login=yes", val r = client.submitForm("$portalURL/auth/?login=yes",
formParameters = parameters { formParameters = parameters {
append("AUTH_FORM", "Y") append("AUTH_FORM", "Y")
@@ -70,43 +151,51 @@ suspend fun getToken(login: String, password: String): String? {
append("USER_PASSWORD", password) append("USER_PASSWORD", password)
} }
) )
if (r.status.value == 302) {
if (r.status.value == 302) PHPSESSID =
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
return null getMyself(login)
getCSRF()
return true
}
return false
} }
suspend fun getId(login: String): Int { /**
return JSONObject(client.get("$ruzapiURL/studentinfo/") { * Save info about current [User] in memory
*/
private suspend fun getMyself(login: String) {
// WARNING: trailing / is important, 'cuz API devs are eating shit
val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo/") {
header("Cookie", "PHPSESSID=$PHPSESSID") header("Cookie", "PHPSESSID=$PHPSESSID")
parameter("uns", login.drop(1)) parameter("uns", login.drop(1))
}.bodyAsText()).getString("id").toInt() }.bodyAsText())
}
suspend fun getUser(userId: Int? = null): User { val user = JSONObject(
// WARNING: trailing / is important, 'cuz API devs are eating shit client.get("$vuzapiURL/user") {
// TODO: make up another way to get unnId: this is not useful for lectures
val json = JSONObject(
client.get("$vuzapiURL/user/${userId ?: ""}") {
header("Cookie", "PHPSESSID=$PHPSESSID") header("Cookie", "PHPSESSID=$PHPSESSID")
}.bodyAsText() }.bodyAsText()
) )
return User( Log.d("studentInfo", studentinfo.toString(2))
unnId = null,
userId = json.getInt("id"), ME = User(
type = if (json.getJSONArray("profiles").getJSONObject(0) unnId = studentinfo.getString("id").toInt(),
.getString("type") == "employee" bitrixId = user.getInt("bitrix_id"),
) Type.employee else Type.student, userId = user.getInt("id"),
email = json.getString("email"), type = when (studentinfo.getString("type")) {
nameRu = json.getString("fullname"), "lecturer" -> Type.Lecturer // ig,,,
nameEn = json.getString("fullname_en"), else -> Type.Student
isMale = json.getString("sex") == "M", },
email = user.getString("email"),
nameRu = user.getString("fullname"),
nameEn = user.getString("fullname_en"),
isMale = user.getString("sex") == "M",
birthday = Instant birthday = Instant
.parse(json.getString("birthdate")) .parse(user.getString("birthdate"))
.atZone(ZoneId.of("Europe/Moscow")) .atZone(ZoneId.of("Europe/Moscow"))
.toLocalDate(), .toLocalDate(),
avatar = json.getJSONObject("photo").let { avatar = user.getJSONObject("photo").let {
AvatarSet( AvatarSet(
it.getString("orig"), it.getString("orig"),
it.getString("thumbnail"), it.getString("thumbnail"),
@@ -116,7 +205,23 @@ suspend fun getUser(userId: Int? = null): User {
) )
} }
suspend fun downloadSchedule( suspend fun getScheduleDay(
type: Type = ME.type,
id: Int = ME.unnId!!,
date: LocalDate
): ArrayList<ScheduleUnit> {
if ((type == ME.type) and (id == ME.unnId!!)) {
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
Log.d("Schedule", schedule.joinToString())
if (schedule.isNotEmpty())
return schedule
}
return getSchedule(type, id, date, date)
}
suspend fun getSchedule(
type: Type = ME.type, type: Type = ME.type,
id: Int = ME.unnId!!, id: Int = ME.unnId!!,
start: LocalDate, start: LocalDate,
@@ -124,13 +229,13 @@ suspend fun downloadSchedule(
): 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.name}/$id") { val r = client.get("$ruzapiURL/schedule/${type.s}/$id") {
parameter("start", start.format(unnDatePattern)) parameter("start", start.format(unnDatePattern))
parameter("finish", finish.format(unnDatePattern)) parameter("finish", finish.format(unnDatePattern))
parameter("lng", "1") parameter("lng", "1")
} }
val json = JSONArray(r.bodyAsText()) val json = JSONArray(r.bodyAsText())
val out = arrayListOf<ScheduleUnit>() val out = arrayListOf<ScheduleUnit>()
for (i in 0 until json.length()) { for (i in 0 until json.length()) {
val unit = json.getJSONObject(i) val unit = json.getJSONObject(i)
@@ -194,6 +299,9 @@ suspend fun downloadSchedule(
) )
} }
if ((type == ME.type) and (id == ME.unnId!!)) {
cacheSchedule(out)
}
return out return out
} }
@@ -205,23 +313,6 @@ suspend fun getCSRF() {
CSRF = JSONObject(r.bodyAsText()).getString("sessid") CSRF = JSONObject(r.bodyAsText()).getString("sessid")
} }
suspend fun getUserByBitrixId(id: Int): User {
withContext(Dispatchers.IO) {
loadUserByBitrixId(id)
}?.let { return it }
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
header("Cookie", "PHPSESSID=$PHPSESSID")
}.bodyAsText()).getInt("id")
getUser(userId).let { user ->
withContext(Dispatchers.IO) {
cacheUser(user)
}
return user
}
}
suspend fun getBlogposts(): ArrayList<Post> { suspend fun getBlogposts(): ArrayList<Post> {
val r = client.get("$prtl2URL/news.php") { val r = client.get("$prtl2URL/news.php") {
header("Cookie", "PHPSESSID=$PHPSESSID") header("Cookie", "PHPSESSID=$PHPSESSID")
@@ -248,3 +339,54 @@ suspend fun getBlogposts(): ArrayList<Post> {
} }
return out return out
} }
suspend fun getUserByBitrixId(id: Int): User {
withContext(Dispatchers.IO) {
loadUserByBitrixId(id)
}?.let { return it }
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
header("Cookie", "PHPSESSID=$PHPSESSID")
}.bodyAsText()).getInt("id")
getUser(userId).let { user ->
withContext(Dispatchers.IO) {
cacheUser(user)
}
return user
}
}
suspend fun getUser(id: Int): User {
val json = JSONObject(
client.get("$vuzapiURL/user/$id") {
header("Cookie", "PHPSESSID=$PHPSESSID")
}.bodyAsText()
)
Log.d("type", json.getJSONArray("profiles").getJSONObject(0).getString("type"))
return User(
unnId = null,
bitrixId = json.getInt("bitrix_id"),
userId = json.getInt("id"),
type = if (json.getJSONArray("profiles").getJSONObject(0)
.getString("type") == "employee"
) Type.Employee else Type.Student,
email = json.getString("email"),
nameRu = json.getString("fullname"),
nameEn = json.getString("fullname_en"),
isMale = json.getString("sex") == "M",
birthday = Instant
.parse(json.getString("birthdate"))
.atZone(ZoneId.of("Europe/Moscow"))
.toLocalDate(),
avatar = json.getJSONObject("photo").let {
AvatarSet(
it.getString("orig"),
it.getString("thumbnail"),
it.getString("small"),
)
}
)
}
+15 -2
View File
@@ -1,12 +1,25 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn package ru.sweetbread.unn
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
class UNNApp : Application() { class UNNApp : Application() {
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) 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
// }
// }
} }
} }
@@ -1,96 +0,0 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import ru.sweetbread.unn.db.cacheSchedule
import ru.sweetbread.unn.db.loadSchedule
import ru.sweetbread.unn.ui.layout.LoginData
import java.time.LocalDate
lateinit var PHPSESSID: String
lateinit var CSRF: String
lateinit var ME: User
/**
* Authorize user by [login] and [password]
*
* Also defines local vars [PHPSESSID] and [ME]
*/
suspend fun auth(
login: String = LoginData.login,
password: String = LoginData.password,
forced: Boolean = false
): Boolean {
if (!forced) {
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
return true
}
val id = getToken(login, password)
if (id != null) {
PHPSESSID = id
getMyself(login)
getCSRF()
return true
}
return false
}
/**
* Save info about current [User] in memory
*/
private suspend fun getMyself(login: String) = coroutineScope {
val idDeferred = async { getId(login) }
val userDeferred = async { getUser() }
val id = idDeferred.await()
val user = userDeferred.await()
ME = User(
unnId = id,
userId = user.userId,
type = Type.student,
email = user.email,
nameRu = user.nameRu,
nameEn = user.nameEn,
isMale = user.isMale,
birthday = user.birthday,
avatar = user.avatar
)
}
suspend fun getScheduleDay(
type: Type = ME.type,
id: Int = ME.unnId!!,
date: LocalDate
): ArrayList<ScheduleUnit> {
if ((type == ME.type) and (id == ME.unnId!!)) {
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
Log.d("Schedule", schedule.joinToString())
if (schedule.isNotEmpty())
return schedule
}
return getSchedule(type, id, date, date)
}
suspend fun getSchedule(
type: Type = ME.type,
id: Int = ME.unnId!!,
start: LocalDate,
finish: LocalDate
): ArrayList<ScheduleUnit> {
val schedule = downloadSchedule(type, id, start, finish)
if ((type == ME.type) and (id == ME.unnId!!))
cacheSchedule(schedule)
return schedule
}
@@ -1,97 +0,0 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api
import ru.sweetbread.unn.R
import splitties.resources.appStr
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
enum class Type(val s: String) {
student(appStr(R.string.student)),
group("group"),
lecturer(appStr(R.string.lecturer)),
auditorium("auditorium"),
employee(appStr(R.string.employee))
}
enum class LecturerRank(val id: Int) {
Assistant(R.string.assistant),
Lecturer(R.string.lecturer),
SLecturer(R.string.slecturer),
AProfessor(R.string.aprofessor)
}
class ScheduleUnit(
val oid: Int,
val auditorium: Auditorium,
val date: LocalDate,
val discipline: Discipline,
val kindOfWork: KindOfWork,
val lecturers: ArrayList<Lecturer>,
val stream: String,
val begin: LocalTime,
val end: LocalTime
)
class Auditorium(
val name: String,
val oid: Int,
val floor: Int,
val building: Building
)
class Building(
val name: String,
val gid: Int,
val oid: Int
)
class Discipline(
val name: String,
val oid: Int,
val type: Int
)
class KindOfWork(
val name: String,
val oid: Int,
val uid: String,
val complexity: Int
)
class Lecturer(
val name: String,
val rank: LecturerRank,
val email: String,
val unnId: Int,
val uid: String
)
class User(
val unnId: Int?,
val userId: Int,
val type: Type,
val email: String,
val nameRu: String,
val nameEn: String,
val isMale: Boolean,
val birthday: LocalDate,
val avatar: AvatarSet
)
class Post(
val id: Int,
val authorId: Int,
val enableComments: Boolean,
val numComments: Int,
val date: LocalDateTime,
val content: String
)
class AvatarSet(
val original: String,
val thumbnail: String,
val small: String
)
@@ -1,14 +1,7 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db package ru.sweetbread.unn.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import splitties.arch.room.roomDb
val cacheDb = roomDb<AppDatabase>(name = "cache") {
fallbackToDestructiveMigration(dropAllTables = true)
}
@Database(entities = [ @Database(entities = [
UserDB::class, UserDB::class,
@@ -1,5 +1,3 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db package ru.sweetbread.unn.db
import android.util.Log import android.util.Log
@@ -11,13 +9,14 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import ru.sweetbread.unn.api.Auditorium import ru.sweetbread.unn.Auditorium
import ru.sweetbread.unn.api.Building import ru.sweetbread.unn.Building
import ru.sweetbread.unn.api.Discipline import ru.sweetbread.unn.Discipline
import ru.sweetbread.unn.api.KindOfWork import ru.sweetbread.unn.KindOfWork
import ru.sweetbread.unn.api.Lecturer import ru.sweetbread.unn.Lecturer
import ru.sweetbread.unn.api.LecturerRank import ru.sweetbread.unn.LecturerRank
import ru.sweetbread.unn.api.ScheduleUnit import ru.sweetbread.unn.ScheduleUnit
import ru.sweetbread.unn.ui.layout.db
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@@ -44,7 +43,7 @@ interface BuildingDao {
} }
fun cacheBuilding(building: Building) { fun cacheBuilding(building: Building) {
cacheDb.buildingDao().insert( db.buildingDao().insert(
BuildingDB( BuildingDB(
building.oid, building.oid,
building.name, building.name,
@@ -55,13 +54,13 @@ fun cacheBuilding(building: Building) {
} }
fun loadBuilding(oid: Int): Building? { fun loadBuilding(oid: Int): Building? {
return cacheDb.buildingDao().get(oid)?.let { return db.buildingDao().get(oid)?.let {
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() ) > LocalDateTime.now()
) { ) {
cacheDb.buildingDao().delete(it) db.buildingDao().delete(it)
return null return null
} }
Building( Building(
@@ -95,7 +94,7 @@ interface AuditoriumDao {
fun cacheAuditorium(auditorium: Auditorium) { fun cacheAuditorium(auditorium: Auditorium) {
cacheBuilding(auditorium.building) cacheBuilding(auditorium.building)
cacheDb.auditoriumDao().insert( db.auditoriumDao().insert(
AuditoriumDB( AuditoriumDB(
auditorium.oid, auditorium.oid,
auditorium.name, auditorium.name,
@@ -107,13 +106,13 @@ fun cacheAuditorium(auditorium: Auditorium) {
} }
fun loadAuditorium(oid: Int): Auditorium? { fun loadAuditorium(oid: Int): Auditorium? {
return cacheDb.auditoriumDao().get(oid)?.let { return db.auditoriumDao().get(oid)?.let {
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() ) > LocalDateTime.now()
) { ) {
cacheDb.auditoriumDao().delete(it) db.auditoriumDao().delete(it)
return null return null
} }
val building = loadBuilding(it.buildingOid) ?: return null val building = loadBuilding(it.buildingOid) ?: return null
@@ -147,7 +146,7 @@ interface DisciplineDao {
} }
fun cacheDiscipline(discipline: Discipline) { fun cacheDiscipline(discipline: Discipline) {
cacheDb.disciplineDao().insert( db.disciplineDao().insert(
DisciplineDB( DisciplineDB(
discipline.oid, discipline.oid,
discipline.name, discipline.name,
@@ -158,13 +157,13 @@ fun cacheDiscipline(discipline: Discipline) {
} }
fun loadDiscipline(oid: Int): Discipline? { fun loadDiscipline(oid: Int): Discipline? {
return cacheDb.disciplineDao().get(oid)?.let { return db.disciplineDao().get(oid)?.let {
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() ) > LocalDateTime.now()
) { ) {
cacheDb.disciplineDao().delete(it) db.disciplineDao().delete(it)
return null return null
} }
@@ -198,7 +197,7 @@ interface KindOfWorkDao {
} }
fun cacheKindOfWork(kindOfWork: KindOfWork) { fun cacheKindOfWork(kindOfWork: KindOfWork) {
cacheDb.kindOfWorkDao().insert( db.kindOfWorkDao().insert(
KindOfWorkDB( KindOfWorkDB(
kindOfWork.oid, kindOfWork.oid,
kindOfWork.name, kindOfWork.name,
@@ -210,13 +209,13 @@ fun cacheKindOfWork(kindOfWork: KindOfWork) {
} }
fun loadKindOfWork(oid: Int): KindOfWork? { fun loadKindOfWork(oid: Int): KindOfWork? {
return cacheDb.kindOfWorkDao().get(oid)?.let { return db.kindOfWorkDao().get(oid)?.let {
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() ) > LocalDateTime.now()
) { ) {
cacheDb.kindOfWorkDao().delete(it) db.kindOfWorkDao().delete(it)
return null return null
} }
@@ -252,7 +251,7 @@ interface LecturerDao {
} }
fun cacheLecturer(lecturer: Lecturer) { fun cacheLecturer(lecturer: Lecturer) {
cacheDb.lecturerDao().insert( db.lecturerDao().insert(
LecturerDB( LecturerDB(
lecturer.unnId, lecturer.unnId,
lecturer.name, lecturer.name,
@@ -265,13 +264,13 @@ fun cacheLecturer(lecturer: Lecturer) {
} }
fun loadLecturer(unnId: Int): Lecturer? { fun loadLecturer(unnId: Int): Lecturer? {
return cacheDb.lecturerDao().get(unnId)?.let { return db.lecturerDao().get(unnId)?.let {
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() ) > LocalDateTime.now()
) { ) {
cacheDb.lecturerDao().delete(it) db.lecturerDao().delete(it)
return null return null
} }
@@ -321,7 +320,7 @@ fun cacheSchedule(item: ScheduleUnit) {
cacheKindOfWork(item.kindOfWork) cacheKindOfWork(item.kindOfWork)
cacheLecturer(item.lecturers[0]) cacheLecturer(item.lecturers[0])
cacheDb.scheduleDao().insert( db.scheduleDao().insert(
ScheduleUnitDB( ScheduleUnitDB(
item.oid, item.oid,
item.date.format(DateTimeFormatter.ISO_DATE), item.date.format(DateTimeFormatter.ISO_DATE),
@@ -348,7 +347,7 @@ fun cacheSchedule(items: ArrayList<ScheduleUnit>) {
} }
fun loadSchedule(oid: Int): ScheduleUnit? { fun loadSchedule(oid: Int): ScheduleUnit? {
cacheDb.scheduleDao().getSchedule(oid)?.let { db.scheduleDao().getSchedule(oid)?.let {
Log.d("load", it.oid.toString()) Log.d("load", it.oid.toString())
if (LocalDateTime.parse( if (LocalDateTime.parse(
it.expiredAt, it.expiredAt,
@@ -356,7 +355,7 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
) < LocalDateTime.now() ) < LocalDateTime.now()
) { ) {
Log.d("delete", it.oid.toString()) Log.d("delete", it.oid.toString())
cacheDb.scheduleDao().delete(it) db.scheduleDao().delete(it)
return null return null
} }
@@ -376,10 +375,10 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
} }
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> { fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE)) db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") } .map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
return ArrayList( return ArrayList(
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE)) db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { loadSchedule(it.oid) } .mapNotNull { loadSchedule(it.oid) }
) )
} }
@@ -1,5 +1,3 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db package ru.sweetbread.unn.db
import android.util.Log import android.util.Log
@@ -10,9 +8,10 @@ import androidx.room.Entity
import androidx.room.Insert import androidx.room.Insert
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import ru.sweetbread.unn.api.AvatarSet import ru.sweetbread.unn.AvatarSet
import ru.sweetbread.unn.api.Type import ru.sweetbread.unn.Type
import ru.sweetbread.unn.api.User import ru.sweetbread.unn.User
import ru.sweetbread.unn.ui.layout.db
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -21,6 +20,7 @@ import java.time.format.DateTimeFormatter
data class UserDB( data class UserDB(
@PrimaryKey val userId: Int, @PrimaryKey val userId: Int,
@ColumnInfo val unnId: Int?, @ColumnInfo val unnId: Int?,
@ColumnInfo val bitrixId: Int,
@ColumnInfo val type: Type, @ColumnInfo val type: Type,
@ColumnInfo val email: String, @ColumnInfo val email: String,
@ColumnInfo val nameRu: String, @ColumnInfo val nameRu: String,
@@ -35,8 +35,8 @@ data class UserDB(
@Dao @Dao
interface UserDao { interface UserDao {
@Query("SELECT * FROM userDB WHERE userId = :userId LIMIT 1") @Query("SELECT * FROM userDB WHERE bitrixId = :bitrixId LIMIT 1")
fun getUserById(userId: Int): UserDB? fun getUserByBitrix(bitrixId: Int): UserDB?
@Insert @Insert
fun insert(user: UserDB) fun insert(user: UserDB)
@@ -48,10 +48,11 @@ interface UserDao {
fun cacheUser(user: User) { fun cacheUser(user: User) {
try { try {
cacheDb.userDao().insert( db.userDao().insert(
UserDB( UserDB(
user.userId, user.userId,
user.unnId, user.unnId,
user.bitrixId,
user.type, user.type,
user.email, user.email,
user.nameRu, user.nameRu,
@@ -69,7 +70,7 @@ fun cacheUser(user: User) {
} }
fun loadUserByBitrixId(bitrixId: Int): User? { fun loadUserByBitrixId(bitrixId: Int): User? {
val user = cacheDb.userDao().getUserById(bitrixId) val user = db.userDao().getUserByBitrix(bitrixId)
Log.d("UserDB", user?.nameEn ?: "None") Log.d("UserDB", user?.nameEn ?: "None")
if (user == null) return null if (user == null) return null
if (LocalDateTime.parse( if (LocalDateTime.parse(
@@ -77,11 +78,12 @@ fun loadUserByBitrixId(bitrixId: Int): User? {
DateTimeFormatter.ISO_LOCAL_DATE_TIME DateTimeFormatter.ISO_LOCAL_DATE_TIME
) < LocalDateTime.now() ) < LocalDateTime.now()
) { ) {
cacheDb.userDao().delete(user) db.userDao().delete(user)
return null return null
} else { } else {
return User( return User(
user.unnId, user.unnId,
user.bitrixId,
user.userId, user.userId,
user.type, user.type,
user.email, user.email,
@@ -1,38 +0,0 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ru.sweetbread.unn.R
import splitties.resources.appStr
@Composable
fun About() {
Column(Modifier.fillMaxSize()) {
Text(
appStr(R.string.developer),
style = MaterialTheme.typography.headlineMedium
)
Column(
Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(16.dp)
)
.padding(16.dp)
) {
Text("meow")
}
}
}
@@ -1,13 +1,12 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). /*
* 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
@@ -44,22 +43,23 @@ 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 coil.request.ImageRequest import com.google.android.material.textview.MaterialTextView
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 ru.sweetbread.unn.AvatarSet
import ru.sweetbread.unn.Post
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.AvatarSet import ru.sweetbread.unn.Type
import ru.sweetbread.unn.api.Post import ru.sweetbread.unn.User
import ru.sweetbread.unn.api.Type import ru.sweetbread.unn.getBlogposts
import ru.sweetbread.unn.api.User import ru.sweetbread.unn.getUserByBitrixId
import ru.sweetbread.unn.api.getBlogposts import ru.sweetbread.unn.portalURL
import ru.sweetbread.unn.api.getUserByBitrixId
import ru.sweetbread.unn.api.portalURL
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -70,7 +70,8 @@ import java.time.format.FormatStyle
val defUser = User( val defUser = User(
null, null,
123, 123,
Type.student, 123,
Type.Student,
"cool.email@domain.com", "cool.email@domain.com",
"Джон Сигма Омегович", "Джон Сигма Омегович",
"Jon Sigma Omega", "Jon Sigma Omega",
@@ -172,6 +173,7 @@ 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("") }
@@ -187,7 +189,7 @@ fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = fals
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 38.dp), .padding(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.surfaceVariant,
trackColor = MaterialTheme.colorScheme.secondary, trackColor = MaterialTheme.colorScheme.secondary,
) )
@@ -196,10 +198,8 @@ 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 = {
TextView(it).apply { MaterialTextView(it).apply {
movementMethod = LinkMovementMethod.getInstance()
autoLinkMask = Linkify.WEB_URLS autoLinkMask = Linkify.WEB_URLS
linksClickable = true linksClickable = true
setTextColor(textColor) setTextColor(textColor)
@@ -208,12 +208,7 @@ 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 = Html.fromHtml( it.text = HtmlCompat.fromHtml(html, 0)
html,
Html.FROM_HTML_MODE_LEGACY,
CoilImageGetter(it),
null
)
} }
) )
@@ -237,9 +232,7 @@ fun UserItemPreview() {
Modifier Modifier
.width(300.dp) .width(300.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primaryContainer), .background(MaterialTheme.colorScheme.primaryContainer), defUser, Type.Student.s
defUser,
Type.student.s
) )
} }
} }
@@ -267,76 +260,3 @@ 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,50 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@Composable
fun GroupsNameText(
text: String,
modifier: Modifier = Modifier,
highlightColor: Color = MaterialTheme.colorScheme.secondaryContainer,
highlightPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
highlightShape: Shape = MaterialTheme.shapes.small,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
) {
val suffix = if (text.endsWith("-В-OUP")) "-В-OUP" else null
val mainPart = if (suffix != null) text.dropLast(6) else text
val blocks = mainPart.split('|')
FlowRow(
modifier = modifier,
horizontalArrangement = horizontalArrangement,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
blocks.forEach { block ->
Box(
modifier = Modifier
.padding(end = 2.dp)
.background(highlightColor, highlightShape)
.padding(highlightPadding)
) {
Text(text = block)
}
}
suffix?.let { Text(text = it) }
}
}
@@ -1,18 +1,18 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
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.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.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
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -43,20 +43,23 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.kizitonwose.calendar.compose.WeekCalendar
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.sweetbread.unn.Auditorium
import ru.sweetbread.unn.Building
import ru.sweetbread.unn.Discipline
import ru.sweetbread.unn.KindOfWork
import ru.sweetbread.unn.Lecturer
import ru.sweetbread.unn.LecturerRank
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.Auditorium import ru.sweetbread.unn.ScheduleUnit
import ru.sweetbread.unn.api.Building import ru.sweetbread.unn.getScheduleDay
import ru.sweetbread.unn.api.Discipline
import ru.sweetbread.unn.api.KindOfWork
import ru.sweetbread.unn.api.Lecturer
import ru.sweetbread.unn.api.LecturerRank
import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.api.getScheduleDay
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import 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
@@ -66,18 +69,35 @@ import java.util.Calendar
@Composable @Composable
fun Schedule() { fun Schedule() {
var selectedDate by remember { mutableStateOf(LocalDate.now()) } val state = rememberWeekCalendarState(
firstDayOfWeek = DayOfWeek.MONDAY // TODO: set start and end weeks to September and July of current year
)
Column { Column {
BoundedWeekPicker( var curDate by remember { mutableStateOf(LocalDate.now()) }
modifier = Modifier WeekCalendar(
.fillMaxWidth() state = state,
.padding(vertical = 8.dp), dayContent = {
selectedDate = selectedDate, Box(
onDateSelected = { selectedDate = it } modifier = Modifier
.padding(vertical = 16.dp)
.aspectRatio(1f) // This is important for square sizing!
.offset(2.dp)
.background(if (it.date == curDate) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.surfaceContainer)
.clickable(
onClick = { curDate = it.date },
enabled = curDate != it.date
),
contentAlignment = Alignment.Center,
) {
Text(
text = it.date.dayOfMonth.toString(),
fontWeight = if (it.date == LocalDate.now()) FontWeight.Bold else null
)
}
}
) )
ScheduleDay(date = curDate)
ScheduleDay(date = selectedDate)
} }
} }
@@ -121,20 +141,21 @@ 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 getRatio(): Float { fun getRel(begin: Long, end: Long, now: Long): Float {
val begin = LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond() if ((begin > now) or (now > end))
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 return -1f
if (now > end)
return 1f
return (now - begin) / (end - begin).toFloat() 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 ratio by remember { mutableFloatStateOf(getRatio()) } var rel by remember {
mutableFloatStateOf(getRel(
LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
))
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
while (true) { while (true) {
@@ -144,25 +165,24 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
val millisUntilNextMinute = (60 - seconds) * 1000L - calendar.get(Calendar.MILLISECOND) val millisUntilNextMinute = (60 - seconds) * 1000L - calendar.get(Calendar.MILLISECOND)
delay(millisUntilNextMinute) delay(millisUntilNextMinute)
ratio = getRatio() rel = getRel(
LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond(),
LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
)
} }
} }
val backgroundColor by animateColorAsState(
targetValue = when (ratio) {
1f -> MaterialTheme.colorScheme.surfaceContainer
-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(backgroundColor) .background(
if (rel != -1f)
MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceContainer
)
.padding(8.dp) .padding(8.dp)
){ ){
Column (Modifier.weight(1f)) { Column (Modifier.weight(1f)) {
@@ -190,10 +210,7 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
AnimatedVisibility (expanded) { AnimatedVisibility (expanded) {
GroupsNameText( Text(text = unit.stream)
unit.stream,
highlightColor = MaterialTheme.colorScheme.inversePrimary
)
} }
} }
@@ -258,20 +275,14 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
Text(begin.toString(), fontWeight = FontWeight.Bold) Text(begin.toString(), fontWeight = FontWeight.Bold)
AnimatedVisibility ( if (rel != -1f)
(0f <= ratio) and (ratio < 1f),
modifier = Modifier
.weight(1f)
.padding(horizontal = 2.dp)
) {
DividerWithMarker( DividerWithMarker(
positionPercentage = ratio, Modifier.weight(1f).padding(horizontal = 2.dp),
positionPercentage = rel,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
thickness = 3.dp, thickness = 3.dp,
markerSize = 8.dp, markerSize = 8.dp,
markerColor = MaterialTheme.colorScheme.primary markerColor = MaterialTheme.colorScheme.primary)
)
}
Text(end.toString()) Text(end.toString())
} }
} }
@@ -291,7 +302,7 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
@Composable @Composable
fun DividerWithMarker( fun DividerWithMarker(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
positionPercentage: Float, positionPercentage: Float, // от 0f до 1f (например, 0.5f = 50%)
color: Color = Color.Gray, color: Color = Color.Gray,
thickness: Dp = 1.dp, thickness: Dp = 1.dp,
markerSize: Dp = 8.dp, markerSize: Dp = 8.dp,
@@ -1,141 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import java.util.Locale
@Composable
fun BoundedWeekPicker(
modifier: Modifier = Modifier,
selectedDate: LocalDate,
onDateSelected: (LocalDate) -> Unit,
baseDate: LocalDate = LocalDate.now()
) {
val rangeStart = remember(baseDate) {
baseDate
.with(DayOfWeek.MONDAY)
.minusWeeks(2)
}
val pageCount = 2 /* prev */ + 1 /* cur */ + 5 /* next */
fun pageIndexForDate(date: LocalDate): Int {
val weekStart = date.with(DayOfWeek.MONDAY)
return ChronoUnit.WEEKS.between(rangeStart, weekStart)
.toInt()
.coerceIn(0, pageCount - 1)
}
val initialPage = remember(selectedDate) { pageIndexForDate(selectedDate) }
val pagerState = rememberPagerState(
initialPage = initialPage,
pageCount = { pageCount }
)
LaunchedEffect(selectedDate) {
val targetPage = pageIndexForDate(selectedDate)
if (targetPage != pagerState.currentPage) {
pagerState.scrollToPage(targetPage)
}
}
HorizontalPager(
state = pagerState,
modifier = modifier,
beyondViewportPageCount = 1,
pageSpacing = 0.dp
) { page ->
val weekStart = rangeStart.plusWeeks(page.toLong())
val days = List(6) { weekStart.plusDays(it.toLong()) }
WeekPage(
days = days,
selectedDate = selectedDate,
onDateSelected = onDateSelected
)
}
}
@Composable
private fun WeekPage(
days: List<LocalDate>,
selectedDate: LocalDate,
onDateSelected: (LocalDate) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
days.forEach { date ->
DayItem(
date = date,
isSelected = date == selectedDate,
onClick = { onDateSelected(date) }
)
}
}
}
@Composable
private fun DayItem(
date: LocalDate,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
val contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
val fontWeight = if (date == LocalDate.now()) FontWeight.ExtraBold else FontWeight.Normal
Box(
modifier = modifier
.size(56.dp)
.clip(CircleShape)
.background(backgroundColor)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
style = MaterialTheme.typography.labelSmall,
color = contentColor,
fontWeight = fontWeight
)
Text(
text = date.dayOfMonth.toString(),
style = MaterialTheme.typography.bodyLarge,
color = contentColor,
fontWeight = fontWeight
)
}
}
}
@@ -1,4 +1,7 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). /*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.sweetbread.unn.ui.layout package ru.sweetbread.unn.ui.layout
@@ -41,7 +44,7 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.auth import ru.sweetbread.unn.auth
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.activities.start import splitties.activities.start
import splitties.preferences.Preferences import splitties.preferences.Preferences
@@ -1,188 +1,132 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.layout package ru.sweetbread.unn.ui.layout
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage import androidx.room.Room
import kotlinx.coroutines.launch import io.ktor.client.HttpClient
import ru.sweetbread.unn.R import io.ktor.client.engine.android.Android
import ru.sweetbread.unn.api.ME import io.ktor.client.plugins.HttpRequestRetry
import ru.sweetbread.unn.api.portalURL import io.ktor.client.plugins.HttpTimeout
import ru.sweetbread.unn.ui.composes.About import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
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.resources.appStr import splitties.arch.room.roomDb
import splitties.toast.toast import splitties.toast.toast
val client = HttpClient(Android) {
install(HttpCache)
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.i("Ktor", message)
}
}
level = LogLevel.ALL
}
install(HttpTimeout) {
connectTimeoutMillis = 5000
}
install(HttpRequestRetry) {
retryOnException(maxRetries = 3, retryOnTimeout = true)
exponentialDelay()
modifyRequest { request ->
request.headers.append("x-retry-count", retryCount.toString())
}
}
}
val db = roomDb<AppDatabase>(name = "database")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
UNNTheme { UNNTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
val navController = rememberNavController() val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) var route by remember { mutableStateOf("portal/blogposts") }
val scope = rememberCoroutineScope()
ModalNavigationDrawer( Scaffold(
drawerState = drawerState, bottomBar = {
drawerContent = { NavigationBar {
DrawerContent( NavigationBarItem(
Modifier onClick = {
.fillMaxHeight() route = "portal/blogposts"
.fillMaxWidth(.75f) navController.navigate(route)
.background(MaterialTheme.colorScheme.surfaceContainer) },
.systemBarsPadding(), icon = {
navController = navController Icon(
) Icons.Filled.Home,
} contentDescription = "Home"
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
when {
currentRoute?.startsWith("portal/") == true ->
appStr(R.string.news)
currentRoute?.startsWith("journal/") == true ->
appStr(R.string.schedule)
else -> appStr(R.string.app_name)
},
Modifier.padding()
) )
}, },
navigationIcon = { selected = route.startsWith("portal/")
IconButton(
onClick = {
scope.launch { drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, "Меню")
}
}
) )
},
bottomBar = {
NavigationBar {
NavigationBarItem(
onClick = {
navController.navigate("portal/blogposts") {
launchSingleTop = true
}
},
icon = {
Icon(
Icons.Filled.Home,
contentDescription = "Home"
)
},
selected = currentRoute?.startsWith("portal/") == true
)
NavigationBarItem( NavigationBarItem(
onClick = { onClick = {
navController.navigate("journal/schedule") { route = "journal/schedule"
launchSingleTop = true navController.navigate(route)
} },
}, icon = {
icon = { Icon(
Icon( Icons.Filled.DateRange,
Icons.Filled.DateRange, contentDescription = "Schedule"
contentDescription = "Schedule" )
) },
}, selected = route.startsWith("journal/")
selected = currentRoute?.startsWith("journal/") == true )
)
NavigationBarItem( NavigationBarItem(
onClick = { toast("Not implemented") }, onClick = { toast("Not implemented") },
icon = { icon = {
Icon( Icon(
Icons.Filled.AccountBox, Icons.Filled.AccountBox,
contentDescription = "Account" contentDescription = "Account"
) )
}, },
selected = false selected = false
) )
}
} }
) { innerPadding -> }
Box(Modifier.padding(innerPadding)) { ) {innerPadding ->
NavHost(navController, startDestination = "journal/schedule") { Box(Modifier.padding(innerPadding)) {
composable("portal/blogposts") { NavHost(navController, startDestination = "portal/blogposts") {
Blogposts() composable("portal/blogposts") {
} Blogposts()
composable("journal/schedule") { }
Schedule() composable("journal/schedule") {
} Schedule()
composable("about") {
About()
}
} }
} }
} }
@@ -191,90 +135,4 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
@Composable
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
Column(modifier = modifier.padding(0.dp)) {
Row(
Modifier
.padding(8.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(16.dp))
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
modifier = Modifier.size(64.dp).clip(CircleShape),
model = portalURL + ME.avatar.thumbnail,
contentDescription = ME.nameEn
)
Column (Modifier.padding(horizontal = 8.dp)) {
Text(
ME.nameRu.split(" ").dropLast(1).joinToString(" "),
fontWeight = FontWeight.Bold
)
Text(ME.type.s)
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
TextButton(
{},
Modifier.fillMaxWidth(),
shape = RectangleShape,
enabled = false
) {
Text(
appStr(R.string.record_book),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
}
TextButton(
{},
Modifier.fillMaxWidth(),
shape = RectangleShape,
enabled = false
) {
Text(
appStr(R.string.documents),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
}
TextButton(
{},
Modifier.fillMaxWidth(),
shape = RectangleShape,
enabled = false
) {
Text(
appStr(R.string.materials),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
}
Spacer(Modifier.weight(1f))
HorizontalDivider()
TextButton(
{navController.navigate("about")},
Modifier.fillMaxWidth(),
shape = RectangleShape
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = "Info",
modifier = Modifier.padding(horizontal = 4.dp)
)
Text(appStr(R.string.about_app))
}
}
}
} }
@@ -1,5 +1,3 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.theme package ru.sweetbread.unn.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -1,14 +1,12 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.theme package ru.sweetbread.unn.ui.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -1,5 +1,3 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.ui.theme package ru.sweetbread.unn.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
@@ -1,172 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<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>
@@ -1,32 +0,0 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<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.

Before

Width:  |  Height:  |  Size: 76 KiB

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
-2
View File
@@ -1,5 +1,3 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<dimen name="activity_horizontal_margin">48dp</dimen> <dimen name="activity_horizontal_margin">48dp</dimen>
</resources> </resources>
-18
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<string name="app_name_reg">ННГУ</string> <string name="app_name_reg">ННГУ</string>
<string name="app_name_dev">ННГУ Альфа</string> <string name="app_name_dev">ННГУ Альфа</string>
@@ -16,20 +14,4 @@
<string name="building">Здание</string> <string name="building">Здание</string>
<string name="floor">Этаж</string> <string name="floor">Этаж</string>
<string name="noData">Нет данных</string> <string name="noData">Нет данных</string>
<string name="news">Новости</string>
<string name="schedule">Расписание</string>
<string name="record_book">Зачётная книга</string>
<string name="about_app">О приложении</string>
<string name="documents">Документы</string>
<string name="materials">Материалы</string>
<string-array name="short_weekdays">
<item>Пн</item>
<item>Вт</item>
<item>Ср</item>
<item>Чт</item>
<item>Пт</item>
<item>Сб</item>
</string-array>
<string name="student">Студент</string>
<string name="employee">Сотрудник</string>
</resources> </resources>
@@ -1,5 +1,3 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<dimen name="activity_horizontal_margin">200dp</dimen> <dimen name="activity_horizontal_margin">200dp</dimen>
</resources> </resources>
@@ -1,5 +1,3 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<dimen name="activity_horizontal_margin">48dp</dimen> <dimen name="activity_horizontal_margin">48dp</dimen>
</resources> </resources>
-2
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<color name="purple_200">#FFBB86FC</color> <color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color> <color name="purple_500">#FF6200EE</color>
-2
View File
@@ -1,5 +1,3 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<!-- Default screen margins, per the Android Design guidelines. --> <!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_horizontal_margin">16dp</dimen>
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<color name="ic_launcher_background">#1565AA</color> <color name="ic_launcher_background">#1565AA</color>
</resources> </resources>
+2 -19
View File
@@ -1,9 +1,8 @@
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<string name="app_name_reg">UNN</string> <string name="app_name_reg">UNN</string>
<string name="app_name_dev">UNN Dev</string> <string name="app_name_dev">UNN Dev</string>
<string name="app_name_beta">UNN Beta</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>
@@ -17,21 +16,5 @@
<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="noData">No Data</string> <string name="noData">No Data</string>
<string name="news">News</string> <!-- <string name="login_failed">"Login failed"</string>-->
<string name="schedule">Schedule</string>
<string name="student">Student</string>
<string name="employee">Employee</string>
<string name="record_book">Record book</string>
<string name="about_app">About app</string>
<string name="documents">Documents</string>
<string name="materials">Materials</string>
<string name="developer">Developer</string>
<string-array name="short_weekdays">
<item>Mn</item>
<item>Tu</item>
<item>We</item>
<item>Th</item>
<item>Fr</item>
<item>Sa</item>
</string-array>
</resources> </resources>
-2
View File
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<resources> <resources>
<style name="Theme.UNN" parent="android:Theme.Material.NoActionBar" /> <style name="Theme.UNN" parent="android:Theme.Material.NoActionBar" />
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<!--
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 -4
View File
@@ -1,11 +1,8 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false
alias(libs.plugins.kotlin.compose) apply false id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
id("com.google.devtools.ksp") version "2.3.2" apply false
} }
buildscript { buildscript {
-12
View File
@@ -1,5 +1,3 @@
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
# Project-wide Gradle settings. # Project-wide Gradle settings.
# IDE (e.g. Android Studio) users: # IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override* # Gradle settings configured through the IDE *will override*
@@ -23,13 +21,3 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
+27 -25
View File
@@ -1,31 +1,33 @@
[versions] [versions]
agp = "9.0.1" acraHttp = "5.11.3"
agp = "8.7.0"
calendar = "2.5.4"
coilCompose = "2.7.0" coilCompose = "2.7.0"
compose = "1.10.3" compose = "1.6.4" # Updating this will cause an error!
coreSplashscreen = "1.2.0" coreSplashscreen = "1.0.1"
datastorePreferences = "1.2.0" datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.1.5" desugar_jdk_libs = "2.1.2"
kotlin = "2.3.10" kotlin = "1.9.0"
coreKtx = "1.17.0" coreKtx = "1.13.1"
junitVersion = "1.3.0" junitVersion = "1.2.1"
espressoCore = "3.7.0" espressoCore = "3.6.1"
ktor = "3.4.0" ktor = "2.3.12"
lifecycle = "2.10.0" lifecycle = "2.8.5"
activityCompose = "1.12.4" activityCompose = "1.9.2"
composeBom = "2026.02.00" composeBom = "2024.03.00" # Updating this will cause an error!
appcompat = "1.7.1" appcompat = "1.7.0"
material = "1.13.0" material = "1.12.0"
annotation = "1.9.1" annotation = "1.8.2"
constraintlayout = "2.2.1" constraintlayout = "2.1.4"
activity = "1.12.4" activity = "1.9.2"
navigationCompose = "2.9.7" navigationCompose = "2.7.7" # Updating this will cause an error!
roomRuntime = "2.8.4" roomRuntime = "2.6.1"
secretsGradlePlugin = "2.0.1" secretsGradlePlugin = "2.0.1"
sentryAndroid = "8.32.0"
splitties = "3.0.0" splitties = "3.0.0"
materialIconsCoreAndroid = "1.7.8" kefirbb = "1.5"
[libraries] [libraries]
acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
@@ -34,6 +36,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
compose = { module = "com.kizitonwose.calendar:compose", version.ref = "calendar" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -59,12 +62,11 @@ 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" }
sentry = { module = "io.sentry:sentry-android", version.ref = "sentryAndroid" }
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splitties" } 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" } 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" } kefirbb = { group = "org.kefirsf", name = "kefirbb", version.ref = "kefirbb" }
[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" }
+1 -3
View File
@@ -1,8 +1,6 @@
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
#Sat Mar 16 18:30:45 MSK 2024 #Sat Mar 16 18:30:45 MSK 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-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
Vendored
+15 -1
View File
@@ -1,6 +1,20 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). #
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################################## ##############################################################################
## ##
-2
View File
@@ -1,5 +1,3 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
pluginManagement { pluginManagement {
repositories { repositories {
google { google {